diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 726393b90..627edd9ed 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,24 +1,17 @@ name: Bug Report -description: Create a report to help us improve -title: "" +description: Help us make Kavita better for everyone by submitting issues you run into while using the program. +title: "Put a short summary of what went wrong here" labels: ["needs-triage"] -assignees: body: - type: markdown attributes: - value: | - Thanks for taking the time to fill out this bug report! - - type: markdown - attributes: - value: | - If you have a feature request, please submit on [Github Discussions](https://github.com/Kareadita/Kavita/discussions/2529). + value: "Thanks for taking the time to fill out this bug report!" - type: textarea id: what-happened attributes: label: What happened? - description: Also tell us, what steps you took so we can try to reproduce. + description: Don't forget to tell us what steps you took so we can try to reproduce. placeholder: Tell us what you see! - value: "" validations: required: true - type: textarea @@ -26,33 +19,35 @@ body: attributes: label: What did you expect? description: What did you expect to happen? - placeholder: Tell us what you expected to see! - value: "" + placeholder: Tell us what you expected to see! Go in as much detail as possible so we can confirm if the behavior is something that is broken. validations: required: true - - type: textarea + - type: dropdown id: version attributes: - label: Version - description: What version are you running? - placeholder: Can be found by going to Server Settings > System - value: "" + label: Kavita Version Number - Don't see your version number listed? Then your install is out of date. Please update and see if your issue still persists. + multiple: false + options: + - 0.7.14 - Stable + - Nightly Testing Branch validations: required: true - type: dropdown id: OS attributes: - label: What OS is Kavita being run on? + label: What operating system is Kavita being hosted from? multiple: false options: - - Docker + - Docker (LSIO Container) + - Docker (Dockerhub Container) + - Docker (Other) - Windows - Linux - Mac - type: dropdown id: desktop-OS attributes: - label: If issue being seen on Desktop, what OS are you running where you see the issue? + label: If the issue is being seen on Desktop, what OS are you running where you see the issue? multiple: false options: - Windows @@ -61,17 +56,18 @@ body: - type: dropdown id: desktop-browsers attributes: - label: If issue being seen in the UI, what browsers are you seeing the problem on? + label: If the issue is being seen in the UI, what browsers are you seeing the problem on? multiple: true options: - Firefox - Chrome - Safari - Microsoft Edge + - Other (List in "Additional Notes" box) - type: dropdown id: mobile-OS attributes: - label: If issue being seen on Mobile, what OS are you running where you see the issue? + label: If the issue is being seen on Mobile, what OS are you running where you see the issue? multiple: false options: - Android @@ -79,7 +75,7 @@ body: - type: dropdown id: mobile-browsers attributes: - label: If issue being seen on UI, what browsers are you seeing the problem on? + label: If the issue is being seen on the UI, what browsers are you seeing the problem on? multiple: true options: - Firefox @@ -97,7 +93,4 @@ body: attributes: label: Additional Notes description: Any other information about the issue not covered in this form? - placeholder: e.g. Running Kavita on a raspberry pi, updating from X version, using LSIO container, etc - value: "" - validations: - required: true + placeholder: e.g. Running Kavita on a Raspberry Pi, updating from X version, using LSIO container, etc diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index ec4bb386b..e9be08116 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1 +1,5 @@ -blank_issues_enabled: false \ No newline at end of file +blank_issues_enabled: false +contact_links: + - name: Feature Requests + url: https://github.com/Kareadita/Kavita/discussions + about: Suggest an idea for the Kavita project diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index d9fab953f..98ce4c439 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -26,48 +26,10 @@ jobs: - name: Install dependencies run: dotnet restore - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - distribution: 'zulu' - java-version: '17' - - uses: actions/upload-artifact@v3 with: name: csproj path: Kavita.Common/Kavita.Common.csproj - - name: Cache SonarCloud packages - uses: actions/cache@v3 - with: - path: ~\sonar\cache - key: ${{ runner.os }}-sonar - restore-keys: ${{ runner.os }}-sonar - - - name: Cache SonarCloud scanner - id: cache-sonar-scanner - uses: actions/cache@v3 - with: - path: .\.sonar\scanner - key: ${{ runner.os }}-sonar-scanner - restore-keys: ${{ runner.os }}-sonar-scanner - - - name: Install SonarCloud scanner - if: steps.cache-sonar-scanner.outputs.cache-hit != 'true' - shell: powershell - run: | - New-Item -Path .\.sonar\scanner -ItemType Directory - dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner - - - name: Sonar Scan - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - shell: powershell - run: | - .\.sonar\scanner\dotnet-sonarscanner begin /k:"Kareadita_Kavita" /o:"kareadita" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" - dotnet build --configuration Release - .\.sonar\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" - - name: Test run: dotnet test --no-restore --verbosity normal diff --git a/.github/workflows/release-workflow.yml b/.github/workflows/release-workflow.yml index dca370460..ca1314e8b 100644 --- a/.github/workflows/release-workflow.yml +++ b/.github/workflows/release-workflow.yml @@ -59,6 +59,13 @@ jobs: id: parse-body run: | body="${{ steps.findPr.outputs.body }}" + body=${body//\'/} + body=${body//'%'/'%25'} + body=${body//$'\n'/'%0A'} + body=${body//$'\r'/'%0D'} + body=${body//$'`'/'%60'} + body=${body//$'>'/'%3E'} + if [[ ${#body} -gt 1870 ]] ; then body=${body:0:1870} body="${body}...and much more. @@ -66,16 +73,9 @@ jobs: Read full changelog: https://github.com/Kareadita/Kavita/releases/latest" fi - body=${body//\'/} - body=${body//'%'/'%25'} - body=${body//$'\n'/'%0A'} - body=${body//$'\r'/'%0D'} - body=${body//$'`'/'%60'} - body=${body//$'>'/'%3E'} echo $body echo "BODY=$body" >> $GITHUB_OUTPUT - - name: Check Out Repo uses: actions/checkout@v3 with: diff --git a/.gitignore b/.gitignore index 7da76a034..bb124fc7f 100644 --- a/.gitignore +++ b/.gitignore @@ -527,8 +527,7 @@ API/config/stats/* API/config/stats/app_stats.json API/config/pre-metadata/ API/config/post-metadata/ -API/config/relations-imported.csv -API/config/relations.csv +API/config/*.csv API.Tests/TestResults/ UI/Web/.vscode/settings.json /API.Tests/Services/Test Data/ArchiveService/CoverImages/output/* diff --git a/API.Benchmark/API.Benchmark.csproj b/API.Benchmark/API.Benchmark.csproj index cbfd1d715..ebc913fe1 100644 --- a/API.Benchmark/API.Benchmark.csproj +++ b/API.Benchmark/API.Benchmark.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/API.Benchmark/TestBenchmark.cs b/API.Benchmark/TestBenchmark.cs index 0b4880690..3b08bbcdf 100644 --- a/API.Benchmark/TestBenchmark.cs +++ b/API.Benchmark/TestBenchmark.cs @@ -25,7 +25,7 @@ public class TestBenchmark { list.Add(new VolumeDto() { - Number = random.Next(10) > 5 ? 1 : 0, + MinNumber = random.Next(10) > 5 ? 1 : 0, Chapters = GenerateChapters() }); } @@ -49,7 +49,7 @@ public class TestBenchmark private static void SortSpecialChapters(IEnumerable volumes) { - foreach (var v in volumes.Where(vDto => vDto.Number == 0)) + foreach (var v in volumes.Where(vDto => vDto.MinNumber == 0)) { v.Chapters = v.Chapters.OrderByNatural(x => x.Range).ToList(); } diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj index f6ceb75d3..5287a124a 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/Converters/CronConverterTests.cs b/API.Tests/Converters/CronConverterTests.cs index 4d26edef7..4e214e8f1 100644 --- a/API.Tests/Converters/CronConverterTests.cs +++ b/API.Tests/Converters/CronConverterTests.cs @@ -1,17 +1,20 @@ using API.Helpers.Converters; +using Hangfire; using Xunit; namespace API.Tests.Converters; - +#nullable enable public class CronConverterTests { [Theory] [InlineData("daily", "0 0 * * *")] [InlineData("disabled", "0 0 31 2 *")] [InlineData("weekly", "0 0 * * 1")] - [InlineData("", "0 0 31 2 *")] - [InlineData("sdfgdf", "")] - public void ConvertTest(string input, string expected) + [InlineData("0 0 31 2 *", "0 0 31 2 *")] + [InlineData("sdfgdf", "sdfgdf")] + [InlineData("* * * * *", "* * * * *")] + [InlineData(null, "0 0 * * *")] // daily + public void ConvertTest(string? input, string expected) { Assert.Equal(expected, CronConverter.ConvertToCronNotation(input)); } diff --git a/API.Tests/Extensions/SeriesExtensionsTests.cs b/API.Tests/Extensions/SeriesExtensionsTests.cs index 6a706e892..c14de4439 100644 --- a/API.Tests/Extensions/SeriesExtensionsTests.cs +++ b/API.Tests/Extensions/SeriesExtensionsTests.cs @@ -192,7 +192,7 @@ public class SeriesExtensionsTests .Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("0") .WithIsSpecial(false) .WithCoverImage("Volume 1") @@ -229,7 +229,7 @@ public class SeriesExtensionsTests .Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("0") .WithIsSpecial(false) .WithCoverImage("Volume 1") @@ -266,14 +266,14 @@ public class SeriesExtensionsTests .Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("0") .WithIsSpecial(false) .WithCoverImage("Volume 1") .Build()) .Build()) .WithVolume(new VolumeBuilder("137") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("0") .WithIsSpecial(false) .WithCoverImage("Volume 137") @@ -306,7 +306,7 @@ public class SeriesExtensionsTests .Build()) .Build()) .WithVolume(new VolumeBuilder("4") - .WithNumber(4) + .WithMinNumber(4) .WithChapter(new ChapterBuilder("0") .WithIsSpecial(false) .WithCoverImage("Volume 4") diff --git a/API.Tests/Extensions/VolumeListExtensionsTests.cs b/API.Tests/Extensions/VolumeListExtensionsTests.cs index 2db82eeda..e64267896 100644 --- a/API.Tests/Extensions/VolumeListExtensionsTests.cs +++ b/API.Tests/Extensions/VolumeListExtensionsTests.cs @@ -27,7 +27,7 @@ public class VolumeListExtensionsTests .Build(), }; - Assert.Equal(volumes[0].Number, volumes.GetCoverImage(MangaFormat.Archive).Number); + Assert.Equal(volumes[0].MinNumber, volumes.GetCoverImage(MangaFormat.Archive).MinNumber); } [Fact] diff --git a/API.Tests/Helpers/RateLimiterTests.cs b/API.Tests/Helpers/RateLimiterTests.cs new file mode 100644 index 000000000..c05ce4e6d --- /dev/null +++ b/API.Tests/Helpers/RateLimiterTests.cs @@ -0,0 +1,79 @@ +using System; +using API.Helpers; +using Xunit; + +namespace API.Tests.Helpers; + +public class RateLimiterTests +{ + [Fact] + public void AcquireTokens_Successful() + { + // Arrange + var limiter = new RateLimiter(3, TimeSpan.FromSeconds(1)); + + // Act & Assert + Assert.True(limiter.TryAcquire("test_key")); + Assert.True(limiter.TryAcquire("test_key")); + Assert.True(limiter.TryAcquire("test_key")); + } + + [Fact] + public void AcquireTokens_ExceedLimit() + { + // Arrange + var limiter = new RateLimiter(2, TimeSpan.FromSeconds(10), false); + + // Act + limiter.TryAcquire("test_key"); + limiter.TryAcquire("test_key"); + + // Assert + Assert.False(limiter.TryAcquire("test_key")); + } + + [Fact] + public void AcquireTokens_Refill() + { + // Arrange + var limiter = new RateLimiter(2, TimeSpan.FromSeconds(1)); + + // Act + limiter.TryAcquire("test_key"); + limiter.TryAcquire("test_key"); + + // Wait for refill + System.Threading.Thread.Sleep(1100); + + // Assert + Assert.True(limiter.TryAcquire("test_key")); + } + + [Fact] + public void AcquireTokens_Refill_WithOff() + { + // Arrange + var limiter = new RateLimiter(2, TimeSpan.FromSeconds(10), false); + + // Act + limiter.TryAcquire("test_key"); + limiter.TryAcquire("test_key"); + + // Wait for refill + System.Threading.Thread.Sleep(2100); + + // Assert + Assert.False(limiter.TryAcquire("test_key")); + } + + [Fact] + public void AcquireTokens_MultipleKeys() + { + // Arrange + var limiter = new RateLimiter(2, TimeSpan.FromSeconds(1)); + + // Act & Assert + Assert.True(limiter.TryAcquire("key1")); + Assert.True(limiter.TryAcquire("key2")); + } +} diff --git a/API.Tests/Parser/MangaParserTests.cs b/API.Tests/Parser/MangaParserTests.cs index 5ba6a35b7..126e781d6 100644 --- a/API.Tests/Parser/MangaParserTests.cs +++ b/API.Tests/Parser/MangaParserTests.cs @@ -292,6 +292,8 @@ public class MangaParserTests [InlineData("Accel World Chapter 001 Volume 002", "1")] [InlineData("Bleach 001-003", "1-3")] [InlineData("Accel World Volume 2", "0")] + [InlineData("Historys Strongest Disciple Kenichi_v11_c90-98", "90-98")] + [InlineData("Historys Strongest Disciple Kenichi c01-c04", "1-4")] public void ParseChaptersTest(string filename, string expected) { Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename)); diff --git a/API.Tests/Services/BookmarkServiceTests.cs b/API.Tests/Services/BookmarkServiceTests.cs index 25c7bd5de..6a82f457d 100644 --- a/API.Tests/Services/BookmarkServiceTests.cs +++ b/API.Tests/Services/BookmarkServiceTests.cs @@ -180,7 +180,7 @@ Substitute.For()); var series = new SeriesBuilder("Test") .WithFormat(MangaFormat.Epub) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("0") .Build()) .Build()) @@ -246,7 +246,7 @@ Substitute.For()); var series = new SeriesBuilder("Test") .WithFormat(MangaFormat.Epub) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1") .Build()) .Build()) @@ -322,7 +322,7 @@ Substitute.For()); var series = new SeriesBuilder("Test") .WithFormat(MangaFormat.Epub) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1") .Build()) .Build()) @@ -375,7 +375,7 @@ Substitute.For()); var series = new SeriesBuilder("Test") .WithFormat(MangaFormat.Epub) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1") .Build()) .Build()) @@ -428,7 +428,7 @@ Substitute.For()); var series = new SeriesBuilder("Test") .WithFormat(MangaFormat.Epub) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1") .Build()) .Build()) diff --git a/API.Tests/Services/CleanupServiceTests.cs b/API.Tests/Services/CleanupServiceTests.cs index 0002b4e6a..8c29c5c18 100644 --- a/API.Tests/Services/CleanupServiceTests.cs +++ b/API.Tests/Services/CleanupServiceTests.cs @@ -395,7 +395,7 @@ public class CleanupServiceTests : AbstractDbTest var series = new SeriesBuilder("Test") .WithFormat(MangaFormat.Epub) .WithVolume(new VolumeBuilder("0") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(c) .Build()) .Build(); @@ -488,15 +488,21 @@ public class CleanupServiceTests : AbstractDbTest var user = new AppUser() { UserName = "CleanupWantToRead_ShouldRemoveFullyReadSeries", - WantToRead = new List() - { - s - } }; _context.AppUser.Add(user); await _unitOfWork.CommitAsync(); + // Add want to read + user.WantToRead = new List() + { + new AppUserWantToRead() + { + SeriesId = s.Id + } + }; + await _unitOfWork.CommitAsync(); + await _readerService.MarkSeriesAsRead(user, s.Id); await _unitOfWork.CommitAsync(); diff --git a/API.Tests/Services/ReaderServiceTests.cs b/API.Tests/Services/ReaderServiceTests.cs index 1200c3097..3134997ff 100644 --- a/API.Tests/Services/ReaderServiceTests.cs +++ b/API.Tests/Services/ReaderServiceTests.cs @@ -136,7 +136,7 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("0") .WithPages(1) .Build()) @@ -166,7 +166,7 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("0") .WithPages(1) .Build()) @@ -205,7 +205,7 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("0") .WithPages(1) .Build()) @@ -260,7 +260,7 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("0") .WithPages(1) .Build()) @@ -299,7 +299,7 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("0") .WithPages(1) .Build()) @@ -347,19 +347,19 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithNumber(2) + .WithMinNumber(2) .WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("22").Build()) .Build()) .WithVolume(new VolumeBuilder("3") - .WithNumber(3) + .WithMinNumber(3) .WithChapter(new ChapterBuilder("31").Build()) .WithChapter(new ChapterBuilder("32").Build()) .Build()) @@ -382,6 +382,40 @@ public class ReaderServiceTests Assert.Equal("2", actualChapter.Range); } + [Fact] + public async Task GetNextChapterIdAsync_ShouldGetNextVolume_WhenUsingRanges() + { + // V1 -> V2 + await ResetDb(); + + var series = new SeriesBuilder("Test") + .WithVolume(new VolumeBuilder("1-2") + .WithMinNumber(1) + .WithChapter(new ChapterBuilder("0").Build()) + .Build()) + + .WithVolume(new VolumeBuilder("3-4") + .WithMinNumber(2) + .WithChapter(new ChapterBuilder("1").Build()) + .Build()) + .Build(); + series.Library = new LibraryBuilder("Test Lib", LibraryType.Manga).Build(); + + _context.Series.Add(series); + + _context.AppUser.Add(new AppUser() + { + UserName = "majora2007" + }); + + await _context.SaveChangesAsync(); + + var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 1, 1); + var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); + Assert.Equal("3-4", actualChapter.Volume.Name); + Assert.Equal("1", actualChapter.Range); + } + [Fact] public async Task GetNextChapterIdAsync_ShouldGetNextVolume_OnlyFloats() { @@ -432,19 +466,19 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithNumber(2) + .WithMinNumber(2) .WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("22").Build()) .Build()) .WithVolume(new VolumeBuilder("3") - .WithNumber(3) + .WithMinNumber(3) .WithChapter(new ChapterBuilder("31").Build()) .WithChapter(new ChapterBuilder("32").Build()) .Build()) @@ -473,19 +507,19 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("1.5") - .WithNumber(2) + .WithMinNumber(2) .WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("22").Build()) .Build()) .WithVolume(new VolumeBuilder("3") - .WithNumber(3) + .WithMinNumber(3) .WithChapter(new ChapterBuilder("31").Build()) .WithChapter(new ChapterBuilder("32").Build()) .Build()) @@ -515,13 +549,13 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("22").Build()) .Build()) @@ -550,18 +584,18 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("66").Build()) .WithChapter(new ChapterBuilder("67").Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithNumber(2) + .WithMinNumber(2) .WithChapter(new ChapterBuilder("0").Build()) .Build()) .Build(); @@ -592,13 +626,13 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) .Build()) @@ -624,7 +658,7 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) @@ -650,7 +684,7 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) @@ -669,6 +703,37 @@ public class ReaderServiceTests Assert.Equal(-1, nextChapter); } + // This is commented out because, while valid, I can't solve how to make this pass (https://github.com/Kareadita/Kavita/issues/2099) + // [Fact] + // public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromLastChapter_NoSpecials_FirstIsVolume() + // { + // await ResetDb(); + // + // var series = new SeriesBuilder("Test") + // .WithVolume(new VolumeBuilder("0") + // .WithMinNumber(0) + // .WithChapter(new ChapterBuilder("1").Build()) + // .WithChapter(new ChapterBuilder("2").Build()) + // .Build()) + // .WithVolume(new VolumeBuilder("1") + // .WithMinNumber(1) + // .WithChapter(new ChapterBuilder("0").Build()) + // .Build()) + // .Build(); + // series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); + // + // _context.Series.Add(series); + // _context.AppUser.Add(new AppUser() + // { + // UserName = "majora2007" + // }); + // + // await _context.SaveChangesAsync(); + // + // var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 3, 1); + // Assert.Equal(-1, nextChapter); + // } + // This is commented out because, while valid, I can't solve how to make this pass // [Fact] // public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromLastChapter_WithSpecials() @@ -677,14 +742,14 @@ public class ReaderServiceTests // // var series = new SeriesBuilder("Test") // .WithVolume(new VolumeBuilder("0") - // .WithNumber(0) + // .WithMinNumber(0) // .WithChapter(new ChapterBuilder("1").Build()) // .WithChapter(new ChapterBuilder("2").Build()) // .WithChapter(new ChapterBuilder("0").WithIsSpecial(true).Build()) // .Build()) // // .WithVolume(new VolumeBuilder("1") - // .WithNumber(1) + // .WithMinNumber(1) // .WithChapter(new ChapterBuilder("2").Build()) // .Build()) // .Build(); @@ -711,13 +776,13 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) .Build()) @@ -747,7 +812,7 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) @@ -778,13 +843,13 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("0").Build()) .Build()) .Build(); @@ -811,12 +876,12 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) .Build()) @@ -846,12 +911,12 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("12").Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithNumber(2) + .WithMinNumber(2) .WithChapter(new ChapterBuilder("12").Build()) .Build()) .Build(); @@ -872,7 +937,7 @@ public class ReaderServiceTests var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 1, 1); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter, ChapterIncludes.Volumes); - Assert.Equal(2, actualChapter.Volume.Number); + Assert.Equal(2, actualChapter.Volume.MinNumber); } #endregion @@ -887,19 +952,19 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithNumber(2) + .WithMinNumber(2) .WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("22").Build()) .Build()) .WithVolume(new VolumeBuilder("3") - .WithNumber(3) + .WithMinNumber(3) .WithChapter(new ChapterBuilder("31").Build()) .WithChapter(new ChapterBuilder("32").Build()) .Build()) @@ -930,19 +995,19 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("1.5") - .WithNumber(2) + .WithMinNumber(2) .WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("22").Build()) .Build()) .WithVolume(new VolumeBuilder("3") - .WithNumber(3) + .WithMinNumber(3) .WithChapter(new ChapterBuilder("31").Build()) .WithChapter(new ChapterBuilder("32").Build()) .Build()) @@ -1054,13 +1119,13 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) .Build()) @@ -1092,7 +1157,7 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) @@ -1122,7 +1187,7 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("0").Build()) .Build()) .Build(); @@ -1151,13 +1216,13 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("0").Build()) .Build()) .Build(); @@ -1186,20 +1251,20 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("5").Build()) .WithChapter(new ChapterBuilder("6").Build()) .WithChapter(new ChapterBuilder("7").Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("2").WithIsSpecial(true).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithNumber(2) + .WithMinNumber(2) .WithChapter(new ChapterBuilder("3").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("4").WithIsSpecial(true).Build()) .Build()) @@ -1234,7 +1299,7 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) @@ -1264,12 +1329,12 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) .Build()) @@ -1302,12 +1367,12 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("22").Build()) .Build()) @@ -1340,12 +1405,12 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("12").Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithNumber(2) + .WithMinNumber(2) .WithChapter(new ChapterBuilder("12").Build()) .Build()) .Build(); @@ -1361,7 +1426,7 @@ public class ReaderServiceTests var nextChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 2, 1); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter, ChapterIncludes.Volumes); - Assert.Equal(1, actualChapter.Volume.Number); + Assert.Equal(1, actualChapter.Volume.MinNumber); } #endregion @@ -1622,6 +1687,35 @@ public class ReaderServiceTests } + + [Fact] + public async Task GetContinuePoint_ShouldReturnFirstChapter_WhenHasSpecial() + { + await ResetDb(); + var series = new SeriesBuilder("Test") + // Loose chapters + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("Prologue").WithIsSpecial(true).WithPages(1).Build()) + .Build()) + .Build(); + series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); + + _context.Series.Add(series); + + _context.AppUser.Add(new AppUser() + { + UserName = "majora2007" + }); + + await _context.SaveChangesAsync(); + + var nextChapter = await _readerService.GetContinuePoint(1, 1); + + Assert.Equal("1", nextChapter.Range); + } + [Fact] public async Task GetContinuePoint_ShouldReturnFirstSpecial() { diff --git a/API.Tests/Services/ReadingListServiceTests.cs b/API.Tests/Services/ReadingListServiceTests.cs index c6d9675d0..23de53674 100644 --- a/API.Tests/Services/ReadingListServiceTests.cs +++ b/API.Tests/Services/ReadingListServiceTests.cs @@ -759,7 +759,7 @@ public class ReadingListServiceTests var fablesSeries = new SeriesBuilder("Fables").Build(); fablesSeries.Volumes.Add( new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithName("2002") .WithChapter(new ChapterBuilder("1").Build()) .Build() @@ -937,7 +937,7 @@ public class ReadingListServiceTests var fables2Series = new SeriesBuilder("Fables: The Last Castle").Build(); fablesSeries.Volumes.Add(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithName("2002") .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) @@ -945,7 +945,7 @@ public class ReadingListServiceTests .Build() ); fables2Series.Volumes.Add(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithName("2003") .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) @@ -980,13 +980,13 @@ public class ReadingListServiceTests var fables2Series = new SeriesBuilder("Fablesa: The Last Castle").Build(); fablesSeries.Volumes.Add(new VolumeBuilder("2002") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("3").Build()) .Build()); fables2Series.Volumes.Add(new VolumeBuilder("2003") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("3").Build()) @@ -1036,7 +1036,7 @@ public class ReadingListServiceTests // Mock up our series var fablesSeries = new SeriesBuilder("Fables") .WithVolume(new VolumeBuilder("2002") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("3").Build()) @@ -1045,7 +1045,7 @@ public class ReadingListServiceTests var fables2Series = new SeriesBuilder("Fables: The Last Castle") .WithVolume(new VolumeBuilder("2003") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("3").Build()) @@ -1094,13 +1094,13 @@ public class ReadingListServiceTests var fables2Series = new SeriesBuilder("Fables: The Last Castle").Build(); fablesSeries.Volumes.Add(new VolumeBuilder("2002") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("3").Build()) .Build()); fables2Series.Volumes.Add(new VolumeBuilder("2003") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("3").Build()) @@ -1153,13 +1153,13 @@ public class ReadingListServiceTests var fables2Series = new SeriesBuilder("Fables: The Last Castle").Build(); fablesSeries.Volumes.Add(new VolumeBuilder("2002") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("3").Build()) .Build()); fables2Series.Volumes.Add(new VolumeBuilder("2003") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("3").Build()) diff --git a/API.Tests/Services/SeriesServiceTests.cs b/API.Tests/Services/SeriesServiceTests.cs index ed5a729ad..97a4306d3 100644 --- a/API.Tests/Services/SeriesServiceTests.cs +++ b/API.Tests/Services/SeriesServiceTests.cs @@ -18,6 +18,7 @@ using API.Services; using API.Services.Plus; using API.SignalR; using API.Tests.Helpers; +using EasyCaching.Core; using Hangfire; using Hangfire.InMemory; using Microsoft.Extensions.Caching.Memory; @@ -1391,4 +1392,96 @@ public class SeriesServiceTests : AbstractDbTest } #endregion + + #region DeleteMultipleSeries + + [Fact] + public async Task DeleteMultipleSeries_ShouldDeleteSeries() + { + await ResetDb(); + var lib1 = new LibraryBuilder("Test LIb") + .WithSeries(new SeriesBuilder("Test Series") + .WithMetadata(new SeriesMetadata() + { + AgeRating = AgeRating.Everyone + }) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("1").WithFile( + new MangaFileBuilder($"{DataDirectory}1.zip", MangaFormat.Archive) + .WithPages(1) + .Build() + ).Build()) + .Build()) + .Build()) + .WithSeries(new SeriesBuilder("Test Series Prequels").Build()) + .WithSeries(new SeriesBuilder("Test Series Sequels").Build()) + .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) + .Build(); + _context.Library.Add(lib1); + + var lib2 = new LibraryBuilder("Test LIb 2", LibraryType.Book) + .WithSeries(new SeriesBuilder("Test Series 2").Build()) + .WithSeries(new SeriesBuilder("Test Series Prequels 2").Build()) + .WithSeries(new SeriesBuilder("Test Series Prequels 2").Build())// TODO: Is this a bug + .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) + .Build(); + _context.Library.Add(lib2); + + await _context.SaveChangesAsync(); + + var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, + SeriesIncludes.Related | SeriesIncludes.ExternalRatings); + // Add relations + var addRelationDto = CreateRelationsDto(series1); + addRelationDto.Adaptations.Add(4); // cross library link + await _seriesService.UpdateRelatedSeries(addRelationDto); + + + // Setup External Metadata stuff + series1.ExternalSeriesMetadata ??= new ExternalSeriesMetadata(); + series1.ExternalSeriesMetadata.ExternalRatings = new List() + { + new ExternalRating() + { + SeriesId = 1, + Provider = ScrobbleProvider.Mal, + AverageScore = 1 + } + }; + series1.ExternalSeriesMetadata.ExternalRecommendations = new List() + { + new ExternalRecommendation() + { + SeriesId = 2, + Name = "Series 2", + Url = "", + CoverUrl = "" + }, + new ExternalRecommendation() + { + SeriesId = 0, // Causes a FK constraint + Name = "Series 2", + Url = "", + CoverUrl = "" + } + }; + series1.ExternalSeriesMetadata.ExternalReviews = new List() + { + new ExternalReview() + { + Body = "", + Provider = ScrobbleProvider.Mal, + BodyJustText = "" + } + }; + + await _context.SaveChangesAsync(); + + // Ensure we can delete the series + Assert.True(await _seriesService.DeleteMultipleSeries(new[] {1, 2})); + Assert.Null(await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1)); + Assert.Null(await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(2)); + } + + #endregion } diff --git a/API/API.csproj b/API/API.csproj index 8ce0a6a57..aee5fa856 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -53,7 +53,9 @@ - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -63,28 +65,27 @@ - - + + - - - + + - + - - - - + + + + - - + + - + @@ -92,17 +93,17 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + @@ -190,6 +191,9 @@ + + Always + diff --git a/API/Constants/CacheProfiles.cs b/API/Constants/CacheProfiles.cs index 9c702a0d8..ee2cd204e 100644 --- a/API/Constants/CacheProfiles.cs +++ b/API/Constants/CacheProfiles.cs @@ -16,11 +16,7 @@ public static class EasyCacheProfiles /// public const string Library = "library"; /// - /// Metadata filter + /// External Series metadata for Kavita+ recommendation /// - public const string Filter = "filter"; - public const string KavitaPlusReviews = "kavita+reviews"; - public const string KavitaPlusRecommendations = "kavita+recommendations"; - public const string KavitaPlusRatings = "kavita+ratings"; public const string KavitaPlusExternalSeries = "kavita+externalSeries"; } diff --git a/API/Constants/PolicyConstants.cs b/API/Constants/PolicyConstants.cs index 69de1821b..de2cf0394 100644 --- a/API/Constants/PolicyConstants.cs +++ b/API/Constants/PolicyConstants.cs @@ -35,7 +35,13 @@ public static class PolicyConstants /// Used to give a user ability to Login to their account /// public const string LoginRole = "Login"; + /// + /// Restricts the ability to manage their account without an admin + /// + /// This is used explicitly for Demo Server. Not sure why it would be used in another fashion + public const string ReadOnlyRole = "Read Only"; + public static readonly ImmutableArray ValidRoles = - ImmutableArray.Create(AdminRole, PlebRole, DownloadRole, ChangePasswordRole, BookmarkRole, ChangeRestrictionRole, LoginRole); + ImmutableArray.Create(AdminRole, PlebRole, DownloadRole, ChangePasswordRole, BookmarkRole, ChangeRestrictionRole, LoginRole, ReadOnlyRole); } diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index c220eb6c0..ab8c19d10 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -77,10 +77,11 @@ public class AccountController : BaseApiController [HttpPost("reset-password")] public async Task UpdatePassword(ResetPasswordDto resetPasswordDto) { - _logger.LogInformation("{UserName} is changing {ResetUser}'s password", User.GetUsername(), resetPasswordDto.UserName); - var user = await _userManager.Users.SingleOrDefaultAsync(x => x.UserName == resetPasswordDto.UserName); if (user == null) return Ok(); // Don't report BadRequest as that would allow brute forcing to find accounts on system + _logger.LogInformation("{UserName} is changing {ResetUser}'s password", User.GetUsername(), resetPasswordDto.UserName); + if (User.IsInRole(PolicyConstants.ReadOnlyRole)) + return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); var isAdmin = User.IsInRole(PolicyConstants.AdminRole); if (resetPasswordDto.UserName == User.GetUsername() && !(User.IsInRole(PolicyConstants.ChangePasswordRole) || isAdmin)) @@ -187,12 +188,14 @@ public class AccountController : BaseApiController { user = await _userManager.Users .Include(u => u.UserPreferences) + .AsSplitQuery() .SingleOrDefaultAsync(x => x.ApiKey == loginDto.ApiKey); } else { user = await _userManager.Users .Include(u => u.UserPreferences) + .AsSplitQuery() .SingleOrDefaultAsync(x => x.NormalizedUserName == loginDto.Username.ToUpperInvariant()); } @@ -319,6 +322,7 @@ public class AccountController : BaseApiController public async Task> ResetApiKey() { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()) ?? throw new KavitaUnauthenticatedUserException(); + if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); user.ApiKey = HashUtil.ApiKey(); if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync()) @@ -334,7 +338,9 @@ public class AccountController : BaseApiController /// - /// Initiates the flow to update a user's email address. The email address is not changed in this API. A confirmation link is sent/dumped which will + /// Initiates the flow to update a user's email address. + /// + /// If email is not setup, then the email address is not changed in this API. A confirmation link is sent/dumped which will /// validate the email. It must be confirmed for the email to update. /// /// @@ -343,7 +349,7 @@ public class AccountController : BaseApiController public async Task UpdateEmail(UpdateEmailDto? dto) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); - if (user == null) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied")); + if (user == null || User.IsInRole(PolicyConstants.ReadOnlyRole)) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied")); if (dto == null || string.IsNullOrEmpty(dto.Email) || string.IsNullOrEmpty(dto.Password)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-payload")); @@ -374,14 +380,26 @@ public class AccountController : BaseApiController return BadRequest(await _localizationService.Translate(User.GetUserId(), "generate-token")); } - user.EmailConfirmed = false; + var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var shouldEmailUser = serverSettings.IsEmailSetup() || !_emailService.IsValidEmail(user.Email); + user.EmailConfirmed = !shouldEmailUser; user.ConfirmationToken = token; await _userManager.UpdateAsync(user); + if (!shouldEmailUser) + { + return Ok(new InviteUserResponse + { + EmailLink = string.Empty, + EmailSent = false + }); + } + + // Send a confirmation email try { - var emailLink = await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email-update", dto.Email); + var emailLink = await _emailService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email-update", dto.Email); _logger.LogCritical("[Update Email]: Email Link for {UserName}: {Link}", user.UserName, emailLink); if (!_emailService.IsValidEmail(user.Email)) @@ -396,30 +414,27 @@ public class AccountController : BaseApiController } - var accessible = await _accountService.CheckIfAccessible(Request); - if (accessible) + try { - try + var invitingUser = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).First().UserName!; + // Email the old address of the update change + BackgroundJob.Enqueue(() => _emailService.SendEmailChangeEmail(new ConfirmationEmailDto() { - // Email the old address of the update change - await _emailService.SendEmailChangeEmail(new ConfirmationEmailDto() - { - EmailAddress = string.IsNullOrEmpty(user.Email) ? dto.Email : user.Email, - InstallId = BuildInfo.Version.ToString(), - InvitingUser = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).First().UserName!, - ServerConfirmationLink = emailLink - }); - } - catch (Exception) - { - /* Swallow exception */ - } + EmailAddress = string.IsNullOrEmpty(user.Email) ? dto.Email : user.Email, + InstallId = BuildInfo.Version.ToString(), + InvitingUser = invitingUser, + ServerConfirmationLink = emailLink + })); + } + catch (Exception) + { + /* Swallow exception */ } return Ok(new InviteUserResponse { EmailLink = string.Empty, - EmailSent = accessible + EmailSent = true }); } catch (Exception ex) @@ -439,7 +454,7 @@ public class AccountController : BaseApiController if (user == null) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied")); var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user); - if (!await _accountService.HasChangeRestrictionRole(user)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); + if (!await _accountService.CanChangeAgeRestriction(user)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); user.AgeRestriction = isAdmin ? AgeRating.NotApplicable : dto.AgeRating; user.AgeRestrictionIncludeUnknowns = isAdmin || dto.IncludeUnknowns; @@ -574,13 +589,12 @@ public class AccountController : BaseApiController if (string.IsNullOrEmpty(user.ConfirmationToken)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "manual-setup-fail")); - return await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", user.Email!, withBaseUrl); + return await _emailService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", user.Email!, withBaseUrl); } /// - /// Invites a user to the server. Will generate a setup link for continuing setup. If the server is not accessible, no - /// email will be sent. + /// Invites a user to the server. Will generate a setup link for continuing setup. If email is not setup, a link will be presented to user to continue setup. /// /// /// @@ -679,15 +693,15 @@ public class AccountController : BaseApiController return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-invite-user")); } - try { - var emailLink = await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", dto.Email); + var emailLink = await _emailService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", dto.Email); _logger.LogCritical("[Invite User]: Email Link for {UserName}: {Link}", user.UserName, emailLink); - if (!_emailService.IsValidEmail(dto.Email)) + var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + if (!_emailService.IsValidEmail(dto.Email) || !settings.IsEmailSetup()) { - _logger.LogInformation("[Invite User] {Email} doesn't appear to be an email, so will not send an email to address", dto.Email.Replace(Environment.NewLine, string.Empty)); + _logger.LogInformation("[Invite User] {Email} doesn't appear to be an email or email is not setup", dto.Email.Replace(Environment.NewLine, string.Empty)); return Ok(new InviteUserResponse { EmailLink = emailLink, @@ -696,22 +710,17 @@ public class AccountController : BaseApiController }); } - var accessible = await _accountService.CheckIfAccessible(Request); - if (accessible) + BackgroundJob.Enqueue(() => _emailService.SendInviteEmail(new ConfirmationEmailDto() { - // Do the email send on a background thread to ensure UI can move forward without having to wait for a timeout when users use fake emails - BackgroundJob.Enqueue(() => _emailService.SendConfirmationEmail(new ConfirmationEmailDto() - { - EmailAddress = dto.Email, - InvitingUser = adminUser.UserName, - ServerConfirmationLink = emailLink - })); - } + EmailAddress = dto.Email, + InvitingUser = adminUser.UserName, + ServerConfirmationLink = emailLink + })); return Ok(new InviteUserResponse { EmailLink = emailLink, - EmailSent = accessible + EmailSent = true }); } catch (Exception ex) @@ -837,7 +846,6 @@ public class AccountController : BaseApiController await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id); - // Perform Login code return Ok(); } @@ -882,6 +890,10 @@ public class AccountController : BaseApiController [EnableRateLimiting("Authentication")] public async Task> ForgotPassword([FromQuery] string email) { + + var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + if (!settings.IsEmailSetup()) return Ok(await _localizationService.Get("en", "email-not-enabled")); + var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(email); if (user == null) { @@ -890,32 +902,34 @@ public class AccountController : BaseApiController } var roles = await _userManager.GetRolesAsync(user); - if (!roles.Any(r => r is PolicyConstants.AdminRole or PolicyConstants.ChangePasswordRole)) + if (!roles.Any(r => r is PolicyConstants.AdminRole or PolicyConstants.ChangePasswordRole or PolicyConstants.ReadOnlyRole)) return Unauthorized(await _localizationService.Translate(user.Id, "permission-denied")); if (string.IsNullOrEmpty(user.Email) || !user.EmailConfirmed) return BadRequest(await _localizationService.Translate(user.Id, "confirm-email")); - var token = await _userManager.GeneratePasswordResetTokenAsync(user); - var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-reset-password", user.Email); - _logger.LogCritical("[Forgot Password]: Email Link for {UserName}: {Link}", user.UserName, emailLink); if (!_emailService.IsValidEmail(user.Email)) { - _logger.LogCritical("[Forgot Password]: User is trying to do a forgot password flow, but their email ({Email}) isn't valid. No email will be send", user.Email); + _logger.LogCritical("[Forgot Password]: User is trying to do a forgot password flow, but their email ({Email}) isn't valid. No email will be send. Admin must change it in UI", user.Email); return Ok(await _localizationService.Translate(user.Id, "invalid-email")); } - if (await _accountService.CheckIfAccessible(Request)) - { - await _emailService.SendPasswordResetEmail(new PasswordResetEmailDto() - { - EmailAddress = user.Email, - ServerConfirmationLink = emailLink, - InstallId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value - }); - return Ok(await _localizationService.Translate(user.Id, "email-sent")); - } - return Ok(await _localizationService.Translate(user.Id, "not-accessible-password")); + var token = await _userManager.GeneratePasswordResetTokenAsync(user); + var emailLink = await _emailService.GenerateEmailLink(Request, token, "confirm-reset-password", user.Email); + user.ConfirmationToken = token; + _unitOfWork.UserRepository.Update(user); + await _unitOfWork.CommitAsync(); + _logger.LogCritical("[Forgot Password]: Email Link for {UserName}: {Link}", user.UserName, emailLink); + + var installId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value; + BackgroundJob.Enqueue(() => _emailService.SendForgotPasswordEmail(new PasswordResetEmailDto() + { + EmailAddress = user.Email, + ServerConfirmationLink = emailLink, + InstallId = installId + })); + + return Ok(await _localizationService.Translate(user.Id, "email-sent")); } [HttpGet("email-confirmed")] @@ -963,9 +977,10 @@ public class AccountController : BaseApiController /// /// /// + [Authorize("RequireAdminRole")] [HttpPost("resend-confirmation-email")] [EnableRateLimiting("Authentication")] - public async Task> ResendConfirmationSendEmail([FromQuery] int userId) + public async Task> ResendConfirmationSendEmail([FromQuery] int userId) { var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); if (user == null) return BadRequest(await _localizationService.Get("en", "no-user")); @@ -976,96 +991,47 @@ public class AccountController : BaseApiController if (user.EmailConfirmed) return BadRequest(await _localizationService.Translate(user.Id, "user-already-confirmed")); var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); - var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-email", user.Email); + user.ConfirmationToken = token; + _unitOfWork.UserRepository.Update(user); + await _unitOfWork.CommitAsync(); + var emailLink = await _emailService.GenerateEmailLink(Request, token, "confirm-email-update", user.Email); _logger.LogCritical("[Email Migration]: Email Link for {UserName}: {Link}", user.UserName, emailLink); if (!_emailService.IsValidEmail(user.Email)) { _logger.LogCritical("[Email Migration]: User {UserName} is trying to resend an invite flow, but their email ({Email}) isn't valid. No email will be send", user.UserName, user.Email); - return BadRequest(await _localizationService.Translate(user.Id, "invalid-email")); } - if (await _accountService.CheckIfAccessible(Request)) + + var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var shouldEmailUser = serverSettings.IsEmailSetup() || !_emailService.IsValidEmail(user.Email); + + if (!shouldEmailUser) { - try + return Ok(new InviteUserResponse() { - await _emailService.SendMigrationEmail(new EmailMigrationDto() - { - EmailAddress = user.Email!, - Username = user.UserName!, - ServerConfirmationLink = emailLink, - InstallId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value - }); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an issue resending invite email"); - return BadRequest(await _localizationService.Translate(user.Id, "generic-invite-email")); - } - return Ok(emailLink); + EmailLink = emailLink, + EmailSent = false, + InvalidEmail = !_emailService.IsValidEmail(user.Email) + }); } - return BadRequest(await _localizationService.Translate(user.Id, "not-accessible")); + BackgroundJob.Enqueue(() => _emailService.SendInviteEmail(new ConfirmationEmailDto() + { + EmailAddress = user.Email!, + InvitingUser = User.GetUsername(), + ServerConfirmationLink = emailLink, + InstallId = serverSettings.InstallId + })); + + return Ok(new InviteUserResponse() + { + EmailLink = emailLink, + EmailSent = true, + InvalidEmail = !_emailService.IsValidEmail(user.Email) + }); } - /// - /// This is similar to invite. Essentially we authenticate the user's password then go through invite email flow - /// - /// - /// - [AllowAnonymous] - [HttpPost("migrate-email")] - public async Task> MigrateEmail(MigrateUserEmailDto dto) - { - // If there is an admin account already, return - var users = await _unitOfWork.UserRepository.GetAdminUsersAsync(); - if (users.Any()) return BadRequest(await _localizationService.Get("en", "admin-already-exists")); - - // Check if there is an existing invite - var emailValidationErrors = await _accountService.ValidateEmail(dto.Email); - if (emailValidationErrors.Any()) - { - var invitedUser = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); - if (await _userManager.IsEmailConfirmedAsync(invitedUser!)) - return BadRequest(await _localizationService.Get("en", "user-already-registered", invitedUser!.UserName)); - - _logger.LogInformation("A user is attempting to login, but hasn't accepted email invite"); - return BadRequest(await _localizationService.Get("en", "user-already-invited")); - } - - - var user = await _userManager.Users - .Include(u => u.UserPreferences) - .SingleOrDefaultAsync(x => x.NormalizedUserName == dto.Username.ToUpper()); - if (user == null) return BadRequest(await _localizationService.Get("en", "invalid-username")); - - var validPassword = await _signInManager.UserManager.CheckPasswordAsync(user, dto.Password); - if (!validPassword) return BadRequest(await _localizationService.Get("en", "bad-credentials")); - - try - { - var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); - - user.Email = dto.Email; - if (!await ConfirmEmailToken(token, user)) return BadRequest(await _localizationService.Get("en", "critical-email-migration")); - _unitOfWork.UserRepository.Update(user); - - await _unitOfWork.CommitAsync(); - - return Ok(); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an issue during email migration. Contact support"); - _unitOfWork.UserRepository.Delete(user); - await _unitOfWork.CommitAsync(); - } - - return BadRequest(await _localizationService.Get("en", "critical-email-migration")); - } - - - private async Task ConfirmEmailToken(string token, AppUser user) { var result = await _userManager.ConfirmEmailAsync(user, token); diff --git a/API/Controllers/DeviceController.cs b/API/Controllers/DeviceController.cs index 3e1b57fec..61a847b6e 100644 --- a/API/Controllers/DeviceController.cs +++ b/API/Controllers/DeviceController.cs @@ -92,18 +92,28 @@ public class DeviceController : BaseApiController return Ok(await _unitOfWork.DeviceRepository.GetDevicesForUserAsync(User.GetUserId())); } + /// + /// Sends a collection of chapters to the user's device + /// + /// + /// [HttpPost("send-to")] public async Task SendToDevice(SendToDeviceDto dto) { if (dto.ChapterIds.Any(i => i < 0)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "ChapterIds")); if (dto.DeviceId < 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "DeviceId")); - if (await _emailService.IsDefaultEmailService()) + var isEmailSetup = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).IsEmailSetupForSendToDevice(); + if (!isEmailSetup) return BadRequest(await _localizationService.Translate(User.GetUserId(), "send-to-kavita-email")); + // // Validate that the device belongs to the user + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.Devices); + if (user == null || user.Devices.All(d => d.Id != dto.DeviceId)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "send-to-unallowed")); + var userId = User.GetUserId(); await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, - MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"), + MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(userId, "send-to-device-status"), "started"), userId); try { @@ -112,16 +122,16 @@ public class DeviceController : BaseApiController } catch (KavitaException ex) { - return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message)); + return BadRequest(await _localizationService.Translate(userId, ex.Message)); } finally { - await _eventHub.SendMessageToAsync(MessageFactory.SendingToDevice, - MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"), + await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, + MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(userId, "send-to-device-status"), "ended"), userId); } - return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-send-to")); + return BadRequest(await _localizationService.Translate(userId, "generic-send-to")); } @@ -132,7 +142,8 @@ public class DeviceController : BaseApiController if (dto.SeriesId <= 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "SeriesId")); if (dto.DeviceId < 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "greater-0", "DeviceId")); - if (await _emailService.IsDefaultEmailService()) + var isEmailSetup = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).IsEmailSetupForSendToDevice(); + if (!isEmailSetup) return BadRequest(await _localizationService.Translate(User.GetUserId(), "send-to-kavita-email")); var userId = User.GetUserId(); @@ -156,7 +167,7 @@ public class DeviceController : BaseApiController } finally { - await _eventHub.SendMessageToAsync(MessageFactory.SendingToDevice, + await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(User.GetUserId(), "send-to-device-status"), "ended"), userId); } @@ -164,8 +175,6 @@ public class DeviceController : BaseApiController return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-send-to")); } - - } diff --git a/API/Controllers/DownloadController.cs b/API/Controllers/DownloadController.cs index b983a2d5c..05fd7ea27 100644 --- a/API/Controllers/DownloadController.cs +++ b/API/Controllers/DownloadController.cs @@ -103,7 +103,7 @@ public class DownloadController : BaseApiController var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId); try { - return await DownloadFiles(files, $"download_{User.GetUsername()}_v{volumeId}", $"{series!.Name} - Volume {volume.Number}.zip"); + return await DownloadFiles(files, $"download_{User.GetUsername()}_v{volumeId}", $"{series!.Name} - Volume {volume.Name}.zip"); } catch (KavitaException ex) { @@ -118,7 +118,7 @@ public class DownloadController : BaseApiController return await _accountService.HasDownloadPermission(user); } - private ActionResult GetFirstFileDownload(IEnumerable files) + private PhysicalFileResult GetFirstFileDownload(IEnumerable files) { var (zipFile, contentType, fileDownloadName) = _downloadService.GetFirstFileDownload(files); return PhysicalFile(zipFile, contentType, Uri.EscapeDataString(fileDownloadName), true); @@ -150,31 +150,40 @@ public class DownloadController : BaseApiController private async Task DownloadFiles(ICollection files, string tempFolder, string downloadName) { + var username = User.GetUsername(); + var filename = Path.GetFileNameWithoutExtension(downloadName); try { await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.DownloadProgressEvent(User.GetUsername(), - Path.GetFileNameWithoutExtension(downloadName), 0F, "started")); + MessageFactory.DownloadProgressEvent(username, + filename, $"Downloading {filename}", 0F, "started")); if (files.Count == 1) { await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.DownloadProgressEvent(User.GetUsername(), - Path.GetFileNameWithoutExtension(downloadName), 1F, "ended")); + MessageFactory.DownloadProgressEvent(username, + filename, $"Downloading {filename}",1F, "ended")); return GetFirstFileDownload(files); } - var filePath = _archiveService.CreateZipForDownload(files.Select(c => c.FilePath), tempFolder); + var filePath = _archiveService.CreateZipFromFoldersForDownload(files.Select(c => c.FilePath).ToList(), tempFolder, ProgressCallback); await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.DownloadProgressEvent(User.GetUsername(), - Path.GetFileNameWithoutExtension(downloadName), 1F, "ended")); + MessageFactory.DownloadProgressEvent(username, + filename, "Download Complete", 1F, "ended")); return PhysicalFile(filePath, DefaultContentType, Uri.EscapeDataString(downloadName), true); + + async Task ProgressCallback(Tuple progressInfo) + { + await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.DownloadProgressEvent(username, filename, $"Extracting {Path.GetFileNameWithoutExtension(progressInfo.Item1)}", + Math.Clamp(progressInfo.Item2, 0F, 1F))); + } } catch (Exception ex) { _logger.LogError(ex, "There was an exception when trying to download files"); await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.DownloadProgressEvent(User.GetUsername(), - Path.GetFileNameWithoutExtension(downloadName), 1F, "ended")); + filename, "Download Complete", 1F, "ended")); throw; } } @@ -216,15 +225,15 @@ public class DownloadController : BaseApiController var filename = $"{series!.Name} - Bookmarks.zip"; await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.DownloadProgressEvent(username, Path.GetFileNameWithoutExtension(filename), 0F)); + MessageFactory.DownloadProgressEvent(username, Path.GetFileNameWithoutExtension(filename), $"Downloading {filename}",0F)); var seriesIds = string.Join("_", downloadBookmarkDto.Bookmarks.Select(b => b.SeriesId).Distinct()); var filePath = _archiveService.CreateZipForDownload(files, $"download_{userId}_{seriesIds}_bookmarks"); await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.DownloadProgressEvent(username, Path.GetFileNameWithoutExtension(filename), 1F)); + MessageFactory.DownloadProgressEvent(username, Path.GetFileNameWithoutExtension(filename), $"Downloading {filename}", 1F)); - return PhysicalFile(filePath, DefaultContentType, System.Web.HttpUtility.UrlEncode(filename), true); + return PhysicalFile(filePath, DefaultContentType, Uri.EscapeDataString(filename), true); } } diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index d043188d8..b4b86dccf 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -112,6 +112,13 @@ public class LibraryController : BaseApiController if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-library")); _logger.LogInformation("Created a new library: {LibraryName}", library.Name); + // Restart Folder watching if on + var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + if (settings.EnableFolderWatching) + { + await _libraryWatcher.RestartWatching(); + } + // Assign all the necessary users with this library side nav var userIds = admins.Select(u => u.Id).Append(User.GetUserId()).ToList(); var userNeedingNewLibrary = (await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.SideNavStreams)) diff --git a/API/Controllers/LicenseController.cs b/API/Controllers/LicenseController.cs index 08a7789ad..30c85d22c 100644 --- a/API/Controllers/LicenseController.cs +++ b/API/Controllers/LicenseController.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using API.Constants; using API.Data; -using API.DTOs.Account; using API.DTOs.License; using API.Entities.Enums; using API.Extensions; @@ -20,7 +19,8 @@ public class LicenseController( IUnitOfWork unitOfWork, ILogger logger, ILicenseService licenseService, - ILocalizationService localizationService) + ILocalizationService localizationService, + ITaskScheduler taskScheduler) : BaseApiController { /// @@ -31,7 +31,9 @@ public class LicenseController( [ResponseCache(CacheProfileName = ResponseCacheProfiles.LicenseCache)] public async Task> HasValidLicense(bool forceCheck = false) { - return Ok(await licenseService.HasActiveLicense(forceCheck)); + var result = await licenseService.HasActiveLicense(forceCheck); + await taskScheduler.ScheduleKavitaPlusTasks(); + return Ok(result); } /// @@ -57,6 +59,7 @@ public class LicenseController( setting.Value = null; unitOfWork.SettingsRepository.Update(setting); await unitOfWork.CommitAsync(); + await taskScheduler.ScheduleKavitaPlusTasks(); return Ok(); } @@ -65,7 +68,11 @@ public class LicenseController( public async Task ResetLicense(UpdateLicenseDto dto) { logger.LogInformation("Resetting license on file for Server"); - if (await licenseService.ResetLicense(dto.License, dto.Email)) return Ok(); + if (await licenseService.ResetLicense(dto.License, dto.Email)) + { + await taskScheduler.ScheduleKavitaPlusTasks(); + return Ok(); + } return BadRequest(localizationService.Translate(User.GetUserId(), "unable-to-reset-k+")); } @@ -82,6 +89,7 @@ public class LicenseController( try { await licenseService.AddLicense(dto.License.Trim(), dto.Email.Trim(), dto.DiscordId); + await taskScheduler.ScheduleKavitaPlusTasks(); } catch (Exception ex) { diff --git a/API/Controllers/MetadataController.cs b/API/Controllers/MetadataController.cs index b3dbb8a01..24dedef47 100644 --- a/API/Controllers/MetadataController.cs +++ b/API/Controllers/MetadataController.cs @@ -8,9 +8,12 @@ using API.Data; using API.DTOs; using API.DTOs.Filtering; using API.DTOs.Metadata; +using API.DTOs.Recommendation; +using API.DTOs.SeriesDetail; using API.Entities.Enums; using API.Extensions; using API.Services; +using API.Services.Plus; using Kavita.Common.Extensions; using Microsoft.AspNetCore.Mvc; @@ -18,16 +21,11 @@ namespace API.Controllers; #nullable enable -public class MetadataController : BaseApiController +public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService localizationService, + IExternalMetadataService metadataService) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; - - public MetadataController(IUnitOfWork unitOfWork, ILocalizationService localizationService) - { - _unitOfWork = unitOfWork; - _localizationService = localizationService; - } + public const string CacheKey = "kavitaPlusSeriesDetail_"; /// /// Fetches genres from the instance @@ -39,12 +37,12 @@ public class MetadataController : BaseApiController public async Task>> GetAllGenres(string? libraryIds) { var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(); - if (ids != null && ids.Count > 0) + if (ids is {Count: > 0}) { - return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(ids, User.GetUserId())); + return Ok(await unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(ids, User.GetUserId())); } - return Ok(await _unitOfWork.GenreRepository.GetAllGenreDtosAsync(User.GetUserId())); + return Ok(await unitOfWork.GenreRepository.GetAllGenreDtosAsync(User.GetUserId())); } /// @@ -53,12 +51,12 @@ public class MetadataController : BaseApiController /// role /// [HttpGet("people-by-role")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"role"})] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = ["role"])] public async Task>> GetAllPeople(PersonRole? role) { return role.HasValue ? - Ok(await _unitOfWork.PersonRepository.GetAllPersonDtosByRoleAsync(User.GetUserId(), role!.Value)) : - Ok(await _unitOfWork.PersonRepository.GetAllPersonDtosAsync(User.GetUserId())); + Ok(await unitOfWork.PersonRepository.GetAllPersonDtosByRoleAsync(User.GetUserId(), role.Value)) : + Ok(await unitOfWork.PersonRepository.GetAllPersonDtosAsync(User.GetUserId())); } /// @@ -67,15 +65,15 @@ public class MetadataController : BaseApiController /// String separated libraryIds or null for all people /// [HttpGet("people")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = ["libraryIds"])] public async Task>> GetAllPeople(string? libraryIds) { var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(); - if (ids != null && ids.Count > 0) + if (ids is {Count: > 0}) { - return Ok(await _unitOfWork.PersonRepository.GetAllPeopleDtosForLibrariesAsync(ids, User.GetUserId())); + return Ok(await unitOfWork.PersonRepository.GetAllPeopleDtosForLibrariesAsync(ids, User.GetUserId())); } - return Ok(await _unitOfWork.PersonRepository.GetAllPersonDtosAsync(User.GetUserId())); + return Ok(await unitOfWork.PersonRepository.GetAllPersonDtosAsync(User.GetUserId())); } /// @@ -84,15 +82,15 @@ public class MetadataController : BaseApiController /// String separated libraryIds or null for all tags /// [HttpGet("tags")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = new []{"libraryIds"})] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Instant, VaryByQueryKeys = ["libraryIds"])] public async Task>> GetAllTags(string? libraryIds) { var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(); - if (ids != null && ids.Count > 0) + if (ids is {Count: > 0}) { - return Ok(await _unitOfWork.TagRepository.GetAllTagDtosForLibrariesAsync(ids, User.GetUserId())); + return Ok(await unitOfWork.TagRepository.GetAllTagDtosForLibrariesAsync(ids, User.GetUserId())); } - return Ok(await _unitOfWork.TagRepository.GetAllTagDtosAsync(User.GetUserId())); + return Ok(await unitOfWork.TagRepository.GetAllTagDtosAsync(User.GetUserId())); } /// @@ -101,14 +99,14 @@ public class MetadataController : BaseApiController /// String separated libraryIds or null for all ratings /// This API is cached for 1 hour, varying by libraryIds /// - [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = new [] {"libraryIds"})] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = ["libraryIds"])] [HttpGet("age-ratings")] public async Task>> GetAllAgeRatings(string? libraryIds) { var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(); - if (ids != null && ids.Count > 0) + if (ids is {Count: > 0}) { - return Ok(await _unitOfWork.LibraryRepository.GetAllAgeRatingsDtosForLibrariesAsync(ids)); + return Ok(await unitOfWork.LibraryRepository.GetAllAgeRatingsDtosForLibrariesAsync(ids)); } return Ok(Enum.GetValues().Select(t => new AgeRatingDto() @@ -131,7 +129,7 @@ public class MetadataController : BaseApiController var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(); if (ids is {Count: > 0}) { - return Ok(_unitOfWork.LibraryRepository.GetAllPublicationStatusesDtosForLibrariesAsync(ids)); + return Ok(unitOfWork.LibraryRepository.GetAllPublicationStatusesDtosForLibrariesAsync(ids)); } return Ok(Enum.GetValues().Select(t => new PublicationStatusDto() @@ -152,10 +150,13 @@ public class MetadataController : BaseApiController public async Task>> GetAllLanguages(string? libraryIds) { var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(); - return Ok(await _unitOfWork.LibraryRepository.GetAllLanguagesForLibrariesAsync(ids)); + return Ok(await unitOfWork.LibraryRepository.GetAllLanguagesForLibrariesAsync(ids)); } - + /// + /// Returns all languages Kavita can accept + /// + /// [HttpGet("all-languages")] [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour)] public IEnumerable GetAllValidLanguages() @@ -177,9 +178,68 @@ public class MetadataController : BaseApiController [HttpGet("chapter-summary")] public async Task> GetChapterSummary(int chapterId) { - if (chapterId <= 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist")); - var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId); - if (chapter == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist")); + // TODO: This doesn't seem used anywhere + if (chapterId <= 0) return BadRequest(await localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist")); + var chapter = await unitOfWork.ChapterRepository.GetChapterAsync(chapterId); + if (chapter == null) return BadRequest(await localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist")); return Ok(chapter.Summary); } + + /// + /// If this Series is on Kavita+ Blacklist, removes it. If already cached, invalidates it. + /// This then attempts to refresh data from Kavita+ for this series. + /// + /// + /// + [HttpPost("force-refresh")] + public async Task ForceRefresh(int seriesId) + { + await metadataService.ForceKavitaPlusRefresh(seriesId); + return Ok(); + } + + /// + /// Fetches the details needed from Kavita+ for Series Detail page + /// + /// This will hit upstream K+ if the data in local db is 2 weeks old + /// Series Id + /// Library Type + /// + [HttpGet("series-detail-plus")] + public async Task> GetKavitaPlusSeriesDetailData(int seriesId, LibraryType libraryType) + { + var userReviews = (await unitOfWork.UserRepository.GetUserRatingDtosForSeriesAsync(seriesId, User.GetUserId())) + .Where(r => !string.IsNullOrEmpty(r.Body)) + .OrderByDescending(review => review.Username.Equals(User.GetUsername()) ? 1 : 0) + .ToList(); + + var ret = await metadataService.GetSeriesDetailPlus(seriesId, libraryType); + + await PrepareSeriesDetail(userReviews, ret); + return Ok(ret); + } + + private async Task PrepareSeriesDetail(List userReviews, SeriesDetailPlusDto ret) + { + var isAdmin = User.IsInRole(PolicyConstants.AdminRole); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId())!; + + userReviews.AddRange(ReviewService.SelectSpectrumOfReviews(ret.Reviews.ToList())); + ret.Reviews = userReviews; + + if (!isAdmin && ret.Recommendations != null && user != null) + { + // Re-obtain owned series and take into account age restriction + ret.Recommendations.OwnedSeries = + await unitOfWork.SeriesRepository.GetSeriesDtoByIdsAsync( + ret.Recommendations.OwnedSeries.Select(s => s.Id), user); + ret.Recommendations.ExternalSeries = new List(); + } + + if (ret.Recommendations != null && user != null) + { + ret.Recommendations.OwnedSeries ??= new List(); + await unitOfWork.SeriesRepository.AddSeriesModifiers(user.Id, ret.Recommendations.OwnedSeries); + } + } } diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index a29daa165..2482ef714 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -301,7 +301,7 @@ public class OpdsController : BaseApiController /// [HttpGet("{apiKey}/smart-filter/{filterId}")] [Produces("application/xml")] - public async Task GetSmartFilter(string apiKey, int filterId) + public async Task GetSmartFilter(string apiKey, int filterId, [FromQuery] int pageNumber = 0) { var userId = await GetUser(apiKey); if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds) @@ -315,7 +315,7 @@ public class OpdsController : BaseApiController SetFeedId(feed, "smartFilter-" + filter.Id); var decodedFilter = SmartFilterHelper.Decode(filter.Filter); - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, UserParams.Default, + var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, GetUserParams(pageNumber), decodedFilter); var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(series.Select(s => s.Id)); @@ -1109,7 +1109,7 @@ public class OpdsController : BaseApiController title += $" - {volume.Name}"; } } - else if (volume.Number != 0) + else if (volume.MinNumber != 0) { title = $"{series.Name} - Volume {volume.Name} - {await _seriesService.FormatChapterTitle(userId, chapter, libraryType)}"; } @@ -1250,7 +1250,7 @@ public class OpdsController : BaseApiController if (progress != null) { link.LastRead = progress.PageNum; - link.LastReadDate = progress.LastModifiedUtc; + link.LastReadDate = progress.LastModifiedUtc.ToString("s"); // Adhere to ISO 8601 } link.IsPageStream = true; return link; diff --git a/API/Controllers/PanelsController.cs b/API/Controllers/PanelsController.cs index 2008b0c8d..c53b68f86 100644 --- a/API/Controllers/PanelsController.cs +++ b/API/Controllers/PanelsController.cs @@ -57,7 +57,7 @@ public class PanelsController : BaseApiController PageNum = 0, ChapterId = chapterId, VolumeId = 0, - SeriesId = 0 + SeriesId = 0, }); return Ok(progress); } diff --git a/API/Controllers/PluginController.cs b/API/Controllers/PluginController.cs index ff33cf8e1..ce2e4eced 100644 --- a/API/Controllers/PluginController.cs +++ b/API/Controllers/PluginController.cs @@ -14,19 +14,9 @@ namespace API.Controllers; #nullable enable -public class PluginController : BaseApiController +public class PluginController(IUnitOfWork unitOfWork, ITokenService tokenService, ILogger logger) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly ITokenService _tokenService; - private readonly ILogger _logger; - - public PluginController(IUnitOfWork unitOfWork, ITokenService tokenService, ILogger logger) - { - _unitOfWork = unitOfWork; - _tokenService = tokenService; - _logger = logger; - } - /// /// Authenticate with the Server given an apiKey. This will log you in by returning the user object and the JWT token. /// @@ -42,11 +32,11 @@ public class PluginController : BaseApiController // NOTE: In order to log information about plugins, we need some Plugin Description information for each request // Should log into access table so we can tell the user var ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString(); - var userAgent = HttpContext.Request.Headers["User-Agent"]; - var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); + var userAgent = HttpContext.Request.Headers.UserAgent; + var userId = await unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); if (userId <= 0) { - _logger.LogInformation("A Plugin ({PluginName}) tried to authenticate with an apiKey that doesn't match. Information {@Information}", pluginName.Replace(Environment.NewLine, string.Empty), new + logger.LogInformation("A Plugin ({PluginName}) tried to authenticate with an apiKey that doesn't match. Information {@Information}", pluginName.Replace(Environment.NewLine, string.Empty), new { IpAddress = ipAddress, UserAgent = userAgent, @@ -54,15 +44,15 @@ public class PluginController : BaseApiController }); throw new KavitaUnauthenticatedUserException(); } - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - _logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({UserId})'s API Key", pluginName.Replace(Environment.NewLine, string.Empty), user!.UserName, userId); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId); + logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({UserId})'s API Key", pluginName.Replace(Environment.NewLine, string.Empty), user!.UserName, userId); return new UserDto { Username = user.UserName!, - Token = await _tokenService.CreateToken(user), - RefreshToken = await _tokenService.CreateRefreshToken(user), + Token = await tokenService.CreateToken(user), + RefreshToken = await tokenService.CreateRefreshToken(user), ApiKey = user.ApiKey, - KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value + KavitaVersion = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value }; } @@ -76,8 +66,8 @@ public class PluginController : BaseApiController [HttpGet("version")] public async Task> GetVersion([Required] string apiKey) { - var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); + var userId = await unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); if (userId <= 0) throw new KavitaUnauthenticatedUserException(); - return Ok((await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value); + return Ok((await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value); } } diff --git a/API/Controllers/RatingController.cs b/API/Controllers/RatingController.cs index e82cb1fbd..a40b6680b 100644 --- a/API/Controllers/RatingController.cs +++ b/API/Controllers/RatingController.cs @@ -20,50 +20,12 @@ namespace API.Controllers; /// public class RatingController : BaseApiController { - private readonly ILicenseService _licenseService; - private readonly IRatingService _ratingService; - private readonly ILogger _logger; private readonly IUnitOfWork _unitOfWork; - private readonly IEasyCachingProvider _cacheProvider; - public const string CacheKey = "rating_"; - public RatingController(ILicenseService licenseService, IRatingService ratingService, - ILogger logger, IEasyCachingProviderFactory cachingProviderFactory, IUnitOfWork unitOfWork) + public RatingController(IUnitOfWork unitOfWork) { - _licenseService = licenseService; - _ratingService = ratingService; - _logger = logger; _unitOfWork = unitOfWork; - _cacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRatings); - } - - /// - /// Get the external ratings for a given series - /// - /// - /// - [HttpGet] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.KavitaPlus, VaryByQueryKeys = new []{"seriesId"})] - public async Task>> GetRating(int seriesId) - { - - if (!await _licenseService.HasActiveLicense()) - { - return Ok(Enumerable.Empty()); - } - - var cacheKey = CacheKey + seriesId; - var results = await _cacheProvider.GetAsync>(cacheKey); - if (results.HasValue) - { - return Ok(results.Value); - } - - var ratings = await _ratingService.GetRatings(seriesId); - await _cacheProvider.SetAsync(cacheKey, ratings, TimeSpan.FromHours(24)); - _logger.LogDebug("Caching external rating for {Key}", cacheKey); - return Ok(ratings); } [HttpGet("overall")] diff --git a/API/Controllers/RecommendedController.cs b/API/Controllers/RecommendedController.cs index 979584032..259b84fd8 100644 --- a/API/Controllers/RecommendedController.cs +++ b/API/Controllers/RecommendedController.cs @@ -1,19 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Constants; +using System.Threading.Tasks; using API.Data; using API.DTOs; -using API.DTOs.Recommendation; using API.Extensions; using API.Helpers; -using API.Services; -using API.Services.Plus; -using EasyCaching.Core; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Caching.Memory; -using Newtonsoft.Json; namespace API.Controllers; @@ -22,56 +12,14 @@ namespace API.Controllers; public class RecommendedController : BaseApiController { private readonly IUnitOfWork _unitOfWork; - private readonly IRecommendationService _recommendationService; - private readonly ILicenseService _licenseService; - private readonly ILocalizationService _localizationService; - private readonly IEasyCachingProvider _cacheProvider; + public const string CacheKey = "recommendation_"; - public RecommendedController(IUnitOfWork unitOfWork, IRecommendationService recommendationService, - ILicenseService licenseService, IEasyCachingProviderFactory cachingProviderFactory, - ILocalizationService localizationService) + public RecommendedController(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; - _recommendationService = recommendationService; - _licenseService = licenseService; - _localizationService = localizationService; - _cacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRecommendations); } - /// - /// For Kavita+ users, this will return recommendations on the server. - /// - /// - /// - [HttpGet("recommendations")] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.KavitaPlus, VaryByQueryKeys = new []{"seriesId"})] - public async Task> GetRecommendations(int seriesId) - { - var userId = User.GetUserId(); - if (!await _licenseService.HasActiveLicense()) - { - return Ok(new RecommendationDto()); - } - - if (!await _unitOfWork.UserRepository.HasAccessToSeries(userId, seriesId)) - { - return BadRequest(await _localizationService.Translate(User.GetUserId(), "series-restricted")); - } - - var cacheKey = $"{CacheKey}-{seriesId}-{userId}"; - var results = await _cacheProvider.GetAsync(cacheKey); - if (results.HasValue) - { - return Ok(results.Value); - } - - var ret = await _recommendationService.GetRecommendationsForSeries(userId, seriesId); - await _cacheProvider.SetAsync(cacheKey, ret, TimeSpan.FromHours(10)); - return Ok(ret); - } - - /// /// Quick Reads are series that should be readable in less than 10 in total and are not Ongoing in release. /// @@ -79,7 +27,7 @@ public class RecommendedController : BaseApiController /// Pagination /// [HttpGet("quick-reads")] - public async Task>> GetQuickReads(int libraryId, [FromQuery] UserParams userParams) + public async Task>> GetQuickReads(int libraryId, [FromQuery] UserParams? userParams) { userParams ??= UserParams.Default; var series = await _unitOfWork.SeriesRepository.GetQuickReads(User.GetUserId(), libraryId, userParams); @@ -95,7 +43,7 @@ public class RecommendedController : BaseApiController /// /// [HttpGet("quick-catchup-reads")] - public async Task>> GetQuickCatchupReads(int libraryId, [FromQuery] UserParams userParams) + public async Task>> GetQuickCatchupReads(int libraryId, [FromQuery] UserParams? userParams) { userParams ??= UserParams.Default; var series = await _unitOfWork.SeriesRepository.GetQuickCatchupReads(User.GetUserId(), libraryId, userParams); @@ -111,7 +59,7 @@ public class RecommendedController : BaseApiController /// Pagination /// [HttpGet("highly-rated")] - public async Task>> GetHighlyRated(int libraryId, [FromQuery] UserParams userParams) + public async Task>> GetHighlyRated(int libraryId, [FromQuery] UserParams? userParams) { var userId = User.GetUserId(); userParams ??= UserParams.Default; @@ -129,7 +77,7 @@ public class RecommendedController : BaseApiController /// Pagination /// [HttpGet("more-in")] - public async Task>> GetMoreIn(int libraryId, int genreId, [FromQuery] UserParams userParams) + public async Task>> GetMoreIn(int libraryId, int genreId, [FromQuery] UserParams? userParams) { var userId = User.GetUserId(); @@ -148,7 +96,7 @@ public class RecommendedController : BaseApiController /// Pagination /// [HttpGet("rediscover")] - public async Task>> GetRediscover(int libraryId, [FromQuery] UserParams userParams) + public async Task>> GetRediscover(int libraryId, [FromQuery] UserParams? userParams) { userParams ??= UserParams.Default; var series = await _unitOfWork.SeriesRepository.GetRediscover(User.GetUserId(), libraryId, userParams); diff --git a/API/Controllers/ReviewController.cs b/API/Controllers/ReviewController.cs index 63ff20407..ae8ce02ee 100644 --- a/API/Controllers/ReviewController.cs +++ b/API/Controllers/ReviewController.cs @@ -1,20 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Linq; using System.Threading.Tasks; -using API.Constants; using API.Data; using API.Data.Repositories; using API.DTOs.SeriesDetail; using API.Extensions; using API.Helpers.Builders; -using API.Services; using API.Services.Plus; using AutoMapper; -using EasyCaching.Core; using Hangfire; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; namespace API.Controllers; @@ -22,109 +16,19 @@ namespace API.Controllers; public class ReviewController : BaseApiController { - private readonly ILogger _logger; private readonly IUnitOfWork _unitOfWork; - private readonly ILicenseService _licenseService; private readonly IMapper _mapper; - private readonly IReviewService _reviewService; private readonly IScrobblingService _scrobblingService; - private readonly IEasyCachingProvider _cacheProvider; - public const string CacheKey = "review_"; - public ReviewController(ILogger logger, IUnitOfWork unitOfWork, ILicenseService licenseService, - IMapper mapper, IReviewService reviewService, IScrobblingService scrobblingService, - IEasyCachingProviderFactory cachingProviderFactory) + public ReviewController(IUnitOfWork unitOfWork, + IMapper mapper, IScrobblingService scrobblingService) { - _logger = logger; _unitOfWork = unitOfWork; - _licenseService = licenseService; _mapper = mapper; - _reviewService = reviewService; _scrobblingService = scrobblingService; - - _cacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusReviews); } - /// - /// Fetches reviews from the server for a given series - /// - /// - [HttpGet] - [ResponseCache(CacheProfileName = ResponseCacheProfiles.KavitaPlus, VaryByQueryKeys = new []{"seriesId"})] - public async Task>> GetReviews(int seriesId) - { - var userId = User.GetUserId(); - var username = User.GetUsername(); - var userRatings = (await _unitOfWork.UserRepository.GetUserRatingDtosForSeriesAsync(seriesId, userId)) - .Where(r => !string.IsNullOrEmpty(r.Body)) - .OrderByDescending(review => review.Username.Equals(username) ? 1 : 0) - .ToList(); - if (!await _licenseService.HasActiveLicense()) - { - return Ok(userRatings); - } - - var cacheKey = CacheKey + seriesId; - IList externalReviews; - - var result = await _cacheProvider.GetAsync>(cacheKey); - if (result.HasValue) - { - externalReviews = result.Value.ToList(); - } - else - { - var reviews = (await _reviewService.GetReviewsForSeries(userId, seriesId)).ToList(); - externalReviews = SelectSpectrumOfReviews(reviews); - - await _cacheProvider.SetAsync(cacheKey, externalReviews, TimeSpan.FromHours(10)); - _logger.LogDebug("Caching external reviews for {Key}", cacheKey); - } - - - // Fetch external reviews and splice them in - userRatings.AddRange(externalReviews); - - - return Ok(userRatings); - } - - private static IList SelectSpectrumOfReviews(IList reviews) - { - IList externalReviews; - var totalReviews = reviews.Count; - - if (totalReviews > 10) - { - var stepSize = Math.Max((totalReviews - 4) / 8, 1); - - var selectedReviews = new List() - { - reviews[0], - reviews[1], - }; - for (var i = 2; i < totalReviews - 2; i += stepSize) - { - selectedReviews.Add(reviews[i]); - - if (selectedReviews.Count >= 8) - break; - } - - selectedReviews.Add(reviews[totalReviews - 2]); - selectedReviews.Add(reviews[totalReviews - 1]); - - externalReviews = selectedReviews; - } - else - { - externalReviews = reviews; - } - - return externalReviews; - } - /// /// Updates the review for a given series /// @@ -157,4 +61,23 @@ public class ReviewController : BaseApiController _scrobblingService.ScrobbleReviewUpdate(user.Id, dto.SeriesId, string.Empty, dto.Body)); return Ok(_mapper.Map(rating)); } + + /// + /// Deletes the user's review for the given series + /// + /// + [HttpDelete] + public async Task DeleteReview(int seriesId) + { + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.Ratings); + if (user == null) return Unauthorized(); + + user.Ratings = user.Ratings.Where(r => r.SeriesId != seriesId).ToList(); + + _unitOfWork.UserRepository.Update(user); + + await _unitOfWork.CommitAsync(); + + return Ok(); + } } diff --git a/API/Controllers/ScrobblingController.cs b/API/Controllers/ScrobblingController.cs index fc32c3a46..9707bbf61 100644 --- a/API/Controllers/ScrobblingController.cs +++ b/API/Controllers/ScrobblingController.cs @@ -39,22 +39,27 @@ public class ScrobblingController : BaseApiController _localizationService = localizationService; } + /// + /// Get the current user's AniList token + /// + /// [HttpGet("anilist-token")] - public async Task GetAniListToken() + public async Task> GetAniListToken() { - // Validate the license - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); if (user == null) return Unauthorized(); return Ok(user.AniListAccessToken); } + /// + /// Update the current user's AniList token + /// + /// + /// [HttpPost("update-anilist-token")] public async Task UpdateAniListToken(AniListUpdateDto dto) { - // Validate the license - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); if (user == null) return Unauthorized(); @@ -71,6 +76,11 @@ public class ScrobblingController : BaseApiController return Ok(); } + /// + /// Checks if the current Scrobbling token for the given Provider has expired for the current user + /// + /// + /// [HttpGet("token-expired")] public async Task> HasTokenExpired(ScrobbleProvider provider) { @@ -159,15 +169,20 @@ public class ScrobblingController : BaseApiController var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.ScrobbleHolds); if (user == null) return Unauthorized(); if (user.ScrobbleHolds.Any(s => s.SeriesId == seriesId)) - return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do")); + return Ok(await _localizationService.Translate(user.Id, "nothing-to-do")); - var seriesHold = new ScrobbleHoldBuilder().WithSeriesId(seriesId).Build(); + var seriesHold = new ScrobbleHoldBuilder() + .WithSeriesId(seriesId) + .Build(); user.ScrobbleHolds.Add(seriesHold); _unitOfWork.UserRepository.Update(user); try { _unitOfWork.UserRepository.Update(user); await _unitOfWork.CommitAsync(); + + // When a hold is placed on a series, clear any pre-existing Scrobble Events + await _scrobblingService.ClearEventsForSeries(user.Id, seriesId); return Ok(); } catch (DbUpdateConcurrencyException ex) diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index df74a54f7..f65ac0b38 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using API.Constants; using API.Data; @@ -39,16 +38,14 @@ public class SeriesController : BaseApiController private readonly ILicenseService _licenseService; private readonly ILocalizationService _localizationService; private readonly IExternalMetadataService _externalMetadataService; - private readonly IEasyCachingProvider _ratingCacheProvider; - private readonly IEasyCachingProvider _reviewCacheProvider; - private readonly IEasyCachingProvider _recommendationCacheProvider; private readonly IEasyCachingProvider _externalSeriesCacheProvider; - private const string CacheKey = "recommendation_"; + private const string CacheKey = "externalSeriesData_"; public SeriesController(ILogger logger, ITaskScheduler taskScheduler, IUnitOfWork unitOfWork, ISeriesService seriesService, ILicenseService licenseService, - IEasyCachingProviderFactory cachingProviderFactory, ILocalizationService localizationService, IExternalMetadataService externalMetadataService) + IEasyCachingProviderFactory cachingProviderFactory, ILocalizationService localizationService, + IExternalMetadataService externalMetadataService) { _logger = logger; _taskScheduler = taskScheduler; @@ -58,9 +55,6 @@ public class SeriesController : BaseApiController _localizationService = localizationService; _externalMetadataService = externalMetadataService; - _ratingCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRatings); - _reviewCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusReviews); - _recommendationCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRecommendations); _externalSeriesCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusExternalSeries); } @@ -128,6 +122,11 @@ public class SeriesController : BaseApiController return Ok(series); } + /// + /// Deletes a series from Kavita + /// + /// + /// If the series was deleted or not [Authorize(Policy = "RequireAdminRole")] [HttpDelete("{seriesId}")] public async Task> DeleteSeries(int seriesId) @@ -145,7 +144,7 @@ public class SeriesController : BaseApiController var username = User.GetUsername(); _logger.LogInformation("Series {@SeriesId} is being deleted by {UserName}", dto.SeriesIds, username); - if (await _seriesService.DeleteMultipleSeries(dto.SeriesIds)) return Ok(); + if (await _seriesService.DeleteMultipleSeries(dto.SeriesIds)) return Ok(true); return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-series-delete")); } @@ -451,19 +450,6 @@ public class SeriesController : BaseApiController if (!await _seriesService.UpdateSeriesMetadata(updateSeriesMetadataDto)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "update-metadata-fail")); - if (await _licenseService.HasActiveLicense()) - { - _logger.LogDebug("Clearing cache as series weblinks may have changed"); - await _reviewCacheProvider.RemoveAsync(ReviewController.CacheKey + updateSeriesMetadataDto.SeriesMetadata.SeriesId); - await _ratingCacheProvider.RemoveAsync(RatingController.CacheKey + updateSeriesMetadataDto.SeriesMetadata.SeriesId); - - var allUsers = (await _unitOfWork.UserRepository.GetAllUsersAsync()).Select(s => s.Id); - foreach (var userId in allUsers) - { - await _recommendationCacheProvider.RemoveAsync(RecommendedController.CacheKey + $"{updateSeriesMetadataDto.SeriesMetadata.SeriesId}-{userId}"); - } - } - return Ok(await _localizationService.Translate(User.GetUserId(), "series-updated")); } @@ -605,7 +591,7 @@ public class SeriesController : BaseApiController await _externalSeriesCacheProvider.SetAsync(cacheKey, ret, TimeSpan.FromMinutes(15)); return Ok(ret); } - catch (Exception ex) + catch (Exception) { return BadRequest("Unable to load External Series details"); } diff --git a/API/Controllers/ServerController.cs b/API/Controllers/ServerController.cs index 46c7dc9f2..d4e1ed59b 100644 --- a/API/Controllers/ServerController.cs +++ b/API/Controllers/ServerController.cs @@ -38,18 +38,16 @@ public class ServerController : BaseApiController private readonly IStatsService _statsService; private readonly ICleanupService _cleanupService; private readonly IScannerService _scannerService; - private readonly IAccountService _accountService; private readonly ITaskScheduler _taskScheduler; private readonly IUnitOfWork _unitOfWork; private readonly IEasyCachingProviderFactory _cachingProviderFactory; private readonly ILocalizationService _localizationService; - private readonly IEmailService _emailService; public ServerController(ILogger logger, - IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, IStatsService statsService, - ICleanupService cleanupService, IScannerService scannerService, IAccountService accountService, + IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, + IStatsService statsService, ICleanupService cleanupService, IScannerService scannerService, ITaskScheduler taskScheduler, IUnitOfWork unitOfWork, IEasyCachingProviderFactory cachingProviderFactory, - ILocalizationService localizationService, IEmailService emailService) + ILocalizationService localizationService) { _logger = logger; _backupService = backupService; @@ -58,12 +56,10 @@ public class ServerController : BaseApiController _statsService = statsService; _cleanupService = cleanupService; _scannerService = scannerService; - _accountService = accountService; _taskScheduler = taskScheduler; _unitOfWork = unitOfWork; _cachingProviderFactory = cachingProviderFactory; _localizationService = localizationService; - _emailService = emailService; } /// @@ -180,15 +176,35 @@ public class ServerController : BaseApiController } } + /// + /// Checks for updates and pushes an event to the UI + /// + /// Some users have websocket issues so this is not always reliable to alert the user + [HttpGet("check-for-updates")] + public async Task CheckForAnnouncements() + { + await _taskScheduler.CheckForUpdate(); + return Ok(); + } + /// /// Checks for updates, if no updates that are > current version installed, returns null /// [HttpGet("check-update")] - public async Task> CheckForUpdates() + public async Task> CheckForUpdates() { return Ok(await _versionUpdaterService.CheckForUpdate()); } + /// + /// Returns how many versions out of date this install is + /// + [HttpGet("check-out-of-date")] + public async Task> CheckHowOutOfDate() + { + return Ok(await _versionUpdaterService.GetNumberOfReleasesBehind()); + } + /// /// Pull the Changelog for Kavita from Github and display @@ -200,18 +216,6 @@ public class ServerController : BaseApiController return Ok(await _versionUpdaterService.GetAllReleases()); } - /// - /// Is this server accessible to the outside net - /// - /// If the instance has the HostName set, this will return true whether or not it is accessible externally - /// - [HttpGet("accessible")] - [AllowAnonymous] - public async Task> IsServerAccessible() - { - return Ok(await _accountService.CheckIfAccessible(Request)); - } - /// /// Returns a list of reoccurring jobs. Scheduled ad-hoc jobs will not be returned. /// @@ -260,35 +264,13 @@ public class ServerController : BaseApiController /// /// [Authorize("RequireAdminRole")] - [HttpPost("bust-review-and-rec-cache")] + [HttpPost("bust-kavitaplus-cache")] public async Task BustReviewAndRecCache() { _logger.LogInformation("Busting Kavita+ Cache"); - var provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusReviews); - await provider.FlushAsync(); - provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRecommendations); - await provider.FlushAsync(); - provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusRatings); - await provider.FlushAsync(); - provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusExternalSeries); + var provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusExternalSeries); await provider.FlushAsync(); return Ok(); } - /// - /// Returns the KavitaEmail version for non-default instances - /// - /// - [Authorize("RequireAdminRole")] - [HttpGet("email-version")] - public async Task> GetEmailVersion() - { - var emailServiceUrl = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)) - .Value; - - if (emailServiceUrl.Equals(EmailService.DefaultApiUrl)) return Ok(null); - - return Ok(await _emailService.GetVersion(emailServiceUrl)); - } - } diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs index 6277d709b..e0339309b 100644 --- a/API/Controllers/SettingsController.cs +++ b/API/Controllers/SettingsController.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using API.Data; using API.DTOs.Email; using API.DTOs.Settings; +using API.Entities; using API.Entities.Enums; using API.Extensions; using API.Helpers.Converters; @@ -13,7 +14,8 @@ using API.Logging; using API.Services; using API.Services.Tasks.Scanner; using AutoMapper; -using Flurl.Http; +using Cronos; +using Hangfire; using Kavita.Common; using Kavita.Common.EnvironmentInfo; using Kavita.Common.Extensions; @@ -58,6 +60,10 @@ public class SettingsController : BaseApiController return Ok(settingsDto.BaseUrl); } + /// + /// Returns the server settings + /// + /// [Authorize(Policy = "RequireAdminRole")] [HttpGet] public async Task> GetSettings() @@ -119,38 +125,15 @@ public class SettingsController : BaseApiController } /// - /// Resets the email service url + /// Is the minimum information setup for Email to work /// /// [Authorize(Policy = "RequireAdminRole")] - [HttpPost("reset-email-url")] - public async Task> ResetEmailServiceUrlSettings() + [HttpGet("is-email-setup")] + public async Task> IsEmailSetup() { - _logger.LogInformation("{UserName} is resetting Email Service Url Setting", User.GetUsername()); - var emailSetting = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl); - emailSetting.Value = EmailService.DefaultApiUrl; - _unitOfWork.SettingsRepository.Update(emailSetting); - - if (!await _unitOfWork.CommitAsync()) - { - await _unitOfWork.RollbackAsync(); - } - - return Ok(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()); - } - - /// - /// Sends a test email from the Email Service. Will not send if email service is the Default Provider - /// - /// - /// - [Authorize(Policy = "RequireAdminRole")] - [HttpPost("test-email-url")] - public async Task> TestEmailServiceUrl(TestEmailDto dto) - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId()); - var emailService = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value; - return Ok(await _emailService.TestConnectivity(dto.Url, user!.Email, !emailService.Equals(EmailService.DefaultApiUrl))); + var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + return Ok(settings.IsEmailSetup()); } @@ -170,7 +153,8 @@ public class SettingsController : BaseApiController if (!updateSettingsDto.BookmarksDirectory.EndsWith("bookmarks") && !updateSettingsDto.BookmarksDirectory.EndsWith("bookmarks/")) { - bookmarkDirectory = _directoryService.FileSystem.Path.Join(updateSettingsDto.BookmarksDirectory, "bookmarks"); + bookmarkDirectory = + _directoryService.FileSystem.Path.Join(updateSettingsDto.BookmarksDirectory, "bookmarks"); } if (string.IsNullOrEmpty(updateSettingsDto.BookmarksDirectory)) @@ -180,42 +164,29 @@ public class SettingsController : BaseApiController foreach (var setting in currentSettings) { - if (setting.Key == ServerSettingKey.TaskBackup && updateSettingsDto.TaskBackup != setting.Value) - { - setting.Value = updateSettingsDto.TaskBackup; - _unitOfWork.SettingsRepository.Update(setting); - } + UpdateSchedulingSettings(setting, updateSettingsDto); - if (setting.Key == ServerSettingKey.TaskScan && updateSettingsDto.TaskScan != setting.Value) - { - setting.Value = updateSettingsDto.TaskScan; - _unitOfWork.SettingsRepository.Update(setting); - } - - if (setting.Key == ServerSettingKey.OnDeckProgressDays && updateSettingsDto.OnDeckProgressDays + string.Empty != setting.Value) + if (setting.Key == ServerSettingKey.OnDeckProgressDays && + updateSettingsDto.OnDeckProgressDays + string.Empty != setting.Value) { setting.Value = updateSettingsDto.OnDeckProgressDays + string.Empty; _unitOfWork.SettingsRepository.Update(setting); } - if (setting.Key == ServerSettingKey.OnDeckUpdateDays && updateSettingsDto.OnDeckUpdateDays + string.Empty != setting.Value) + if (setting.Key == ServerSettingKey.OnDeckUpdateDays && + updateSettingsDto.OnDeckUpdateDays + string.Empty != setting.Value) { setting.Value = updateSettingsDto.OnDeckUpdateDays + string.Empty; _unitOfWork.SettingsRepository.Update(setting); } - if (setting.Key == ServerSettingKey.CoverImageSize && updateSettingsDto.CoverImageSize + string.Empty != setting.Value) + if (setting.Key == ServerSettingKey.CoverImageSize && + updateSettingsDto.CoverImageSize + string.Empty != setting.Value) { setting.Value = updateSettingsDto.CoverImageSize + string.Empty; _unitOfWork.SettingsRepository.Update(setting); } - if (setting.Key == ServerSettingKey.TaskScan && updateSettingsDto.TaskScan != setting.Value) - { - setting.Value = updateSettingsDto.TaskScan; - _unitOfWork.SettingsRepository.Update(setting); - } - if (setting.Key == ServerSettingKey.Port && updateSettingsDto.Port + string.Empty != setting.Value) { if (OsInfo.IsDocker) continue; @@ -225,7 +196,8 @@ public class SettingsController : BaseApiController _unitOfWork.SettingsRepository.Update(setting); } - if (setting.Key == ServerSettingKey.CacheSize && updateSettingsDto.CacheSize + string.Empty != setting.Value) + if (setting.Key == ServerSettingKey.CacheSize && + updateSettingsDto.CacheSize + string.Empty != setting.Value) { setting.Value = updateSettingsDto.CacheSize + string.Empty; // CacheSize is managed in appSetting.json @@ -233,14 +205,21 @@ public class SettingsController : BaseApiController _unitOfWork.SettingsRepository.Update(setting); } + UpdateEmailSettings(setting, updateSettingsDto); + + + if (setting.Key == ServerSettingKey.IpAddresses && updateSettingsDto.IpAddresses != setting.Value) { if (OsInfo.IsDocker) continue; // Validate IP addresses - foreach (var ipAddress in updateSettingsDto.IpAddresses.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)) + foreach (var ipAddress in updateSettingsDto.IpAddresses.Split(',', + StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)) { - if (!IPAddress.TryParse(ipAddress.Trim(), out _)) { - return BadRequest(await _localizationService.Translate(User.GetUserId(), "ip-address-invalid", ipAddress)); + if (!IPAddress.TryParse(ipAddress.Trim(), out _)) + { + return BadRequest(await _localizationService.Translate(User.GetUserId(), "ip-address-invalid", + ipAddress)); } } @@ -263,20 +242,23 @@ public class SettingsController : BaseApiController _unitOfWork.SettingsRepository.Update(setting); } - if (setting.Key == ServerSettingKey.LoggingLevel && updateSettingsDto.LoggingLevel + string.Empty != setting.Value) + if (setting.Key == ServerSettingKey.LoggingLevel && + updateSettingsDto.LoggingLevel + string.Empty != setting.Value) { setting.Value = updateSettingsDto.LoggingLevel + string.Empty; LogLevelOptions.SwitchLogLevel(updateSettingsDto.LoggingLevel); _unitOfWork.SettingsRepository.Update(setting); } - if (setting.Key == ServerSettingKey.EnableOpds && updateSettingsDto.EnableOpds + string.Empty != setting.Value) + if (setting.Key == ServerSettingKey.EnableOpds && + updateSettingsDto.EnableOpds + string.Empty != setting.Value) { setting.Value = updateSettingsDto.EnableOpds + string.Empty; _unitOfWork.SettingsRepository.Update(setting); } - if (setting.Key == ServerSettingKey.EncodeMediaAs && updateSettingsDto.EncodeMediaAs + string.Empty != setting.Value) + if (setting.Key == ServerSettingKey.EncodeMediaAs && + updateSettingsDto.EncodeMediaAs + string.Empty != setting.Value) { setting.Value = updateSettingsDto.EncodeMediaAs + string.Empty; _unitOfWork.SettingsRepository.Update(setting); @@ -289,23 +271,13 @@ public class SettingsController : BaseApiController _unitOfWork.SettingsRepository.Update(setting); } - if (setting.Key == ServerSettingKey.EmailServiceUrl && updateSettingsDto.EmailServiceUrl + string.Empty != setting.Value) - { - setting.Value = string.IsNullOrEmpty(updateSettingsDto.EmailServiceUrl) ? EmailService.DefaultApiUrl : updateSettingsDto.EmailServiceUrl; - setting.Value = UrlHelper.RemoveEndingSlash(setting.Value); - FlurlHttp.ConfigureClient(setting.Value, cli => - cli.Settings.HttpClientFactory = new UntrustedCertClientFactory()); - - _unitOfWork.SettingsRepository.Update(setting); - } - - if (setting.Key == ServerSettingKey.BookmarkDirectory && bookmarkDirectory != setting.Value) { // Validate new directory can be used if (!await _directoryService.CheckWriteAccess(bookmarkDirectory)) { - return BadRequest(await _localizationService.Translate(User.GetUserId(), "bookmark-dir-permissions")); + return BadRequest( + await _localizationService.Translate(User.GetUserId(), "bookmark-dir-permissions")); } originalBookmarkDirectory = setting.Value; @@ -316,7 +288,8 @@ public class SettingsController : BaseApiController } - if (setting.Key == ServerSettingKey.AllowStatCollection && updateSettingsDto.AllowStatCollection + string.Empty != setting.Value) + if (setting.Key == ServerSettingKey.AllowStatCollection && + updateSettingsDto.AllowStatCollection + string.Empty != setting.Value) { setting.Value = updateSettingsDto.AllowStatCollection + string.Empty; _unitOfWork.SettingsRepository.Update(setting); @@ -330,27 +303,32 @@ public class SettingsController : BaseApiController } } - if (setting.Key == ServerSettingKey.TotalBackups && updateSettingsDto.TotalBackups + string.Empty != setting.Value) + if (setting.Key == ServerSettingKey.TotalBackups && + updateSettingsDto.TotalBackups + string.Empty != setting.Value) { if (updateSettingsDto.TotalBackups > 30 || updateSettingsDto.TotalBackups < 1) { return BadRequest(await _localizationService.Translate(User.GetUserId(), "total-backups")); } + setting.Value = updateSettingsDto.TotalBackups + string.Empty; _unitOfWork.SettingsRepository.Update(setting); } - if (setting.Key == ServerSettingKey.TotalLogs && updateSettingsDto.TotalLogs + string.Empty != setting.Value) + if (setting.Key == ServerSettingKey.TotalLogs && + updateSettingsDto.TotalLogs + string.Empty != setting.Value) { if (updateSettingsDto.TotalLogs > 30 || updateSettingsDto.TotalLogs < 1) { return BadRequest(await _localizationService.Translate(User.GetUserId(), "total-logs")); } + setting.Value = updateSettingsDto.TotalLogs + string.Empty; _unitOfWork.SettingsRepository.Update(setting); } - if (setting.Key == ServerSettingKey.EnableFolderWatching && updateSettingsDto.EnableFolderWatching + string.Empty != setting.Value) + if (setting.Key == ServerSettingKey.EnableFolderWatching && + updateSettingsDto.EnableFolderWatching + string.Empty != setting.Value) { setting.Value = updateSettingsDto.EnableFolderWatching + string.Empty; _unitOfWork.SettingsRepository.Update(setting); @@ -392,6 +370,97 @@ public class SettingsController : BaseApiController return Ok(updateSettingsDto); } + private void UpdateSchedulingSettings(ServerSetting setting, ServerSettingDto updateSettingsDto) + { + if (setting.Key == ServerSettingKey.TaskBackup && updateSettingsDto.TaskBackup != setting.Value) + { + setting.Value = updateSettingsDto.TaskBackup; + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.TaskScan && updateSettingsDto.TaskScan != setting.Value) + { + setting.Value = updateSettingsDto.TaskScan; + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.TaskCleanup && updateSettingsDto.TaskCleanup != setting.Value) + { + setting.Value = updateSettingsDto.TaskCleanup; + _unitOfWork.SettingsRepository.Update(setting); + } + } + + private void UpdateEmailSettings(ServerSetting setting, ServerSettingDto updateSettingsDto) + { + if (setting.Key == ServerSettingKey.EmailHost && + updateSettingsDto.SmtpConfig.Host + string.Empty != setting.Value) + { + setting.Value = updateSettingsDto.SmtpConfig.Host + string.Empty; + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.EmailPort && + updateSettingsDto.SmtpConfig.Port + string.Empty != setting.Value) + { + setting.Value = updateSettingsDto.SmtpConfig.Port + string.Empty; + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.EmailAuthPassword && + updateSettingsDto.SmtpConfig.Password + string.Empty != setting.Value) + { + setting.Value = updateSettingsDto.SmtpConfig.Password + string.Empty; + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.EmailAuthUserName && + updateSettingsDto.SmtpConfig.UserName + string.Empty != setting.Value) + { + setting.Value = updateSettingsDto.SmtpConfig.UserName + string.Empty; + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.EmailSenderAddress && + updateSettingsDto.SmtpConfig.SenderAddress + string.Empty != setting.Value) + { + setting.Value = updateSettingsDto.SmtpConfig.SenderAddress + string.Empty; + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.EmailSenderDisplayName && + updateSettingsDto.SmtpConfig.SenderDisplayName + string.Empty != setting.Value) + { + setting.Value = updateSettingsDto.SmtpConfig.SenderDisplayName + string.Empty; + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.EmailSizeLimit && + updateSettingsDto.SmtpConfig.SizeLimit + string.Empty != setting.Value) + { + setting.Value = updateSettingsDto.SmtpConfig.SizeLimit + string.Empty; + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.EmailEnableSsl && + updateSettingsDto.SmtpConfig.EnableSsl + string.Empty != setting.Value) + { + setting.Value = updateSettingsDto.SmtpConfig.EnableSsl + string.Empty; + _unitOfWork.SettingsRepository.Update(setting); + } + + if (setting.Key == ServerSettingKey.EmailCustomizedTemplates && + updateSettingsDto.SmtpConfig.CustomizedTemplates + string.Empty != setting.Value) + { + setting.Value = updateSettingsDto.SmtpConfig.CustomizedTemplates + string.Empty; + _unitOfWork.SettingsRepository.Update(setting); + } + } + + /// + /// All values allowed for Task Scheduling APIs. A custom cron job is not included. Disabled is not applicable for Cleanup. + /// + /// [Authorize(Policy = "RequireAdminRole")] [HttpGet("task-frequencies")] public ActionResult> GetTaskFrequencies() @@ -410,7 +479,7 @@ public class SettingsController : BaseApiController [HttpGet("log-levels")] public ActionResult> GetLogLevels() { - return Ok(new [] {"Trace", "Debug", "Information", "Warning", "Critical"}); + return Ok(new[] {"Trace", "Debug", "Information", "Warning", "Critical"}); } [HttpGet("opds-enabled")] @@ -419,4 +488,28 @@ public class SettingsController : BaseApiController var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); return Ok(settingsDto.EnableOpds); } + + /// + /// Is the cron expression valid for Kavita's scheduler + /// + /// + /// + [HttpGet("is-valid-cron")] + public ActionResult IsValidCron(string cronExpression) + { + // NOTE: This must match Hangfire's underlying cron system. Hangfire is unique + return Ok(CronHelper.IsValidCron(cronExpression)); + } + + /// + /// Sends a test email to see if email settings are hooked up correctly + /// + /// + [Authorize(Policy = "RequireAdminRole")] + [HttpPost("test-email-url")] + public async Task> TestEmailServiceUrl() + { + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId()); + return Ok(await _emailService.SendTestEmail(user!.Email)); + } } diff --git a/API/Controllers/WantToReadController.cs b/API/Controllers/WantToReadController.cs index 116215a36..b80607b56 100644 --- a/API/Controllers/WantToReadController.cs +++ b/API/Controllers/WantToReadController.cs @@ -7,6 +7,7 @@ using API.DTOs; using API.DTOs.Filtering; using API.DTOs.Filtering.v2; using API.DTOs.WantToRead; +using API.Entities; using API.Extensions; using API.Helpers; using API.Services; @@ -91,15 +92,15 @@ public class WantToReadController : BaseApiController AppUserIncludes.WantToRead); if (user == null) return Unauthorized(); - var existingIds = user.WantToRead.Select(s => s.Id).ToList(); - existingIds.AddRange(dto.SeriesIds); + var existingIds = user.WantToRead.Select(s => s.SeriesId).ToList(); + var idsToAdd = dto.SeriesIds.Except(existingIds); - var idsToAdd = existingIds.Distinct().ToList(); - - var seriesToAdd = await _unitOfWork.SeriesRepository.GetSeriesByIdsAsync(idsToAdd); - foreach (var series in seriesToAdd) + foreach (var id in idsToAdd) { - user.WantToRead.Add(series); + user.WantToRead.Add(new AppUserWantToRead() + { + SeriesId = id + }); } if (!_unitOfWork.HasChanges()) return Ok(); @@ -127,7 +128,9 @@ public class WantToReadController : BaseApiController AppUserIncludes.WantToRead); if (user == null) return Unauthorized(); - user.WantToRead = user.WantToRead.Where(s => !dto.SeriesIds.Contains(s.Id)).ToList(); + user.WantToRead = user.WantToRead + .Where(s => !dto.SeriesIds.Contains(s.SeriesId)) + .ToList(); if (!_unitOfWork.HasChanges()) return Ok(); if (await _unitOfWork.CommitAsync()) diff --git a/API/DTOs/Email/EmailTestResultDto.cs b/API/DTOs/Email/EmailTestResultDto.cs index 6659e3a45..263e725c4 100644 --- a/API/DTOs/Email/EmailTestResultDto.cs +++ b/API/DTOs/Email/EmailTestResultDto.cs @@ -7,4 +7,5 @@ public class EmailTestResultDto { public bool Successful { get; set; } public string ErrorMessage { get; set; } = default!; + public string EmailAddress { get; set; } = default!; } diff --git a/API/DTOs/Filtering/SortField.cs b/API/DTOs/Filtering/SortField.cs index f30b617df..b072819f4 100644 --- a/API/DTOs/Filtering/SortField.cs +++ b/API/DTOs/Filtering/SortField.cs @@ -30,4 +30,8 @@ public enum SortField /// Last time the user had any reading progress /// ReadProgress = 7, + /// + /// Kavita+ Only - External Average Rating + /// + AverageRating = 8 } diff --git a/API/DTOs/Filtering/v2/FilterField.cs b/API/DTOs/Filtering/v2/FilterField.cs index 563c8e4a0..1efb385fa 100644 --- a/API/DTOs/Filtering/v2/FilterField.cs +++ b/API/DTOs/Filtering/v2/FilterField.cs @@ -45,5 +45,9 @@ public enum FilterField /// Last time User Read /// ReadingDate = 27, + /// + /// Average rating from Kavita+ - Not usable for non-licensed users + /// + AverageRating = 28 } diff --git a/API/DTOs/OPDS/FeedLink.cs b/API/DTOs/OPDS/FeedLink.cs index 2a9053f16..cff3b6736 100644 --- a/API/DTOs/OPDS/FeedLink.cs +++ b/API/DTOs/OPDS/FeedLink.cs @@ -39,7 +39,7 @@ public class FeedLink /// /// Attribute MUST conform Atom's Date construct [XmlAttribute("lastReadDate", Namespace = "http://vaemendis.net/opds-pse/ns")] - public DateTime LastReadDate { get; set; } + public string LastReadDate { get; set; } public bool ShouldSerializeLastReadDate() { diff --git a/API/DTOs/Scrobbling/MediaRecommendationDto.cs b/API/DTOs/Scrobbling/MediaRecommendationDto.cs new file mode 100644 index 000000000..c83694b2b --- /dev/null +++ b/API/DTOs/Scrobbling/MediaRecommendationDto.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using API.Services.Plus; + +namespace API.DTOs.Scrobbling; + +public record MediaRecommendationDto +{ + public int Rating { get; set; } + public IEnumerable RecommendationNames { get; set; } = null!; + public string Name { get; set; } + public string CoverUrl { get; set; } + public string SiteUrl { get; set; } + public string? Summary { get; set; } + public int? AniListId { get; set; } + public long? MalId { get; set; } + public ScrobbleProvider Provider { get; set; } +} diff --git a/API/DTOs/Scrobbling/PlusSeriesDto.cs b/API/DTOs/Scrobbling/PlusSeriesDto.cs new file mode 100644 index 000000000..552a86575 --- /dev/null +++ b/API/DTOs/Scrobbling/PlusSeriesDto.cs @@ -0,0 +1,21 @@ +namespace API.DTOs.Scrobbling; + +public record PlusSeriesDto +{ + public int? AniListId { get; set; } + public long? MalId { get; set; } + public string? GoogleBooksId { get; set; } + public string? MangaDexId { get; set; } + public string SeriesName { get; set; } + public string? AltSeriesName { get; set; } + public MediaFormat MediaFormat { get; set; } + /// + /// Optional but can help with matching + /// + public int? ChapterCount { get; set; } + /// + /// Optional but can help with matching + /// + public int? VolumeCount { get; set; } + public int? Year { get; set; } +} diff --git a/API/DTOs/Scrobbling/ScrobbleEventDto.cs b/API/DTOs/Scrobbling/ScrobbleEventDto.cs index 25690da82..298e32180 100644 --- a/API/DTOs/Scrobbling/ScrobbleEventDto.cs +++ b/API/DTOs/Scrobbling/ScrobbleEventDto.cs @@ -8,11 +8,13 @@ public class ScrobbleEventDto public int SeriesId { get; set; } public int LibraryId { get; set; } public bool IsProcessed { get; set; } - public int? VolumeNumber { get; set; } + public float? VolumeNumber { get; set; } public int? ChapterNumber { get; set; } public DateTime LastModifiedUtc { get; set; } public DateTime CreatedUtc { get; set; } public float? Rating { get; set; } public ScrobbleEventType ScrobbleEventType { get; set; } + public bool IsErrored { get; set; } + public string? ErrorDetails { get; set; } } diff --git a/API/DTOs/SeriesDetail/NextExpectedChapterDto.cs b/API/DTOs/SeriesDetail/NextExpectedChapterDto.cs index df4cc1a07..0f1a8eb4b 100644 --- a/API/DTOs/SeriesDetail/NextExpectedChapterDto.cs +++ b/API/DTOs/SeriesDetail/NextExpectedChapterDto.cs @@ -5,7 +5,7 @@ namespace API.DTOs.SeriesDetail; public class NextExpectedChapterDto { public float ChapterNumber { get; set; } - public int VolumeNumber { get; set; } + public float VolumeNumber { get; set; } /// /// Null if not applicable /// diff --git a/API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs b/API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs new file mode 100644 index 000000000..59ce47bf6 --- /dev/null +++ b/API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using API.DTOs.Recommendation; + +namespace API.DTOs.SeriesDetail; + +/// +/// All the data from Kavita+ for Series Detail +/// +/// This is what the UI sees, not what the API sends back +public class SeriesDetailPlusDto +{ + public RecommendationDto? Recommendations { get; set; } + public IEnumerable Reviews { get; set; } + public IEnumerable? Ratings { get; set; } +} diff --git a/API/DTOs/SeriesDetail/UserReviewDto.cs b/API/DTOs/SeriesDetail/UserReviewDto.cs index c8b2f88c5..0e080d43f 100644 --- a/API/DTOs/SeriesDetail/UserReviewDto.cs +++ b/API/DTOs/SeriesDetail/UserReviewDto.cs @@ -14,27 +14,29 @@ public class UserReviewDto /// /// This is not possible to set as a local user public string? Tagline { get; set; } - /// /// The main review /// public string Body { get; set; } - + /// + /// The main body with just text, for review preview + /// + public string? BodyJustText { get; set; } /// /// The series this is for /// public int SeriesId { get; set; } - /// /// The library this series belongs in /// public int LibraryId { get; set; } - /// /// The user who wrote this /// public string Username { get; set; } - + public int TotalVotes { get; set; } + public float Rating { get; set; } + public string? RawBody { get; set; } /// /// How many upvotes this review has gotten /// @@ -43,16 +45,11 @@ public class UserReviewDto /// /// If External, the url of the review /// - public string? ExternalUrl { get; set; } + public string? SiteUrl { get; set; } /// /// Does this review come from an external Source /// public bool IsExternal { get; set; } - /// - /// The main body with just text, for review preview - /// - public string? BodyJustText { get; set; } - /// /// If this review is External, which Provider did it come from /// diff --git a/API/DTOs/Settings/SMTPConfigDto.cs b/API/DTOs/Settings/SMTPConfigDto.cs new file mode 100644 index 000000000..07cc58cb8 --- /dev/null +++ b/API/DTOs/Settings/SMTPConfigDto.cs @@ -0,0 +1,20 @@ +namespace API.DTOs.Settings; + +public class SmtpConfigDto +{ + public string SenderAddress { get; set; } = string.Empty; + public string SenderDisplayName { get; set; } = string.Empty; + public string UserName { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public string Host { get; set; } = string.Empty; + public int Port { get; set; } = 0; + public bool EnableSsl { get; set; } = true; + /// + /// Limit in bytes for allowing files to be added as attachments. Defaults to 25MB + /// + public int SizeLimit { get; set; } = 26_214_400; + /// + /// Should Kavita use config/templates for Email templates or the default ones + /// + public bool CustomizedTemplates { get; set; } = false; +} diff --git a/API/DTOs/Settings/ServerSettingDTO.cs b/API/DTOs/Settings/ServerSettingDTO.cs index e405758bc..077ffbaac 100644 --- a/API/DTOs/Settings/ServerSettingDTO.cs +++ b/API/DTOs/Settings/ServerSettingDTO.cs @@ -8,11 +8,12 @@ public class ServerSettingDto public string CacheDirectory { get; set; } = default!; public string TaskScan { get; set; } = default!; + public string TaskBackup { get; set; } = default!; + public string TaskCleanup { get; set; } = default!; /// /// Logging level for server. Managed in appsettings.json. /// public string LoggingLevel { get; set; } = default!; - public string TaskBackup { get; set; } = default!; /// /// Port the server listens on. Managed in appsettings.json. /// @@ -38,11 +39,6 @@ public class ServerSettingDto /// /// If null or empty string, will default back to default install setting aka public string BookmarksDirectory { get; set; } = default!; - /// - /// Email service to use for the invite user flow, forgot password, etc. - /// - /// If null or empty string, will default back to default install setting aka - public string EmailServiceUrl { get; set; } = default!; public string InstallVersion { get; set; } = default!; /// /// Represents a unique Id to this Kavita installation. Only used in Stats to identify unique installs. @@ -88,4 +84,29 @@ public class ServerSettingDto /// How large the cover images should be /// public CoverImageSize CoverImageSize { get; set; } + /// + /// SMTP Configuration + /// + public SmtpConfigDto SmtpConfig { get; set; } + + /// + /// Are at least some basics filled in + /// + /// + public bool IsEmailSetup() + { + return !string.IsNullOrEmpty(SmtpConfig.Host) + && !string.IsNullOrEmpty(SmtpConfig.UserName) + && !string.IsNullOrEmpty(HostName); + } + + /// + /// Are at least some basics filled in, but not hostname as not required for Send to Device + /// + /// + public bool IsEmailSetupForSendToDevice() + { + return !string.IsNullOrEmpty(SmtpConfig.Host) + && !string.IsNullOrEmpty(SmtpConfig.UserName); + } } diff --git a/API/DTOs/Update/UpdateNotificationDto.cs b/API/DTOs/Update/UpdateNotificationDto.cs index 95719bb27..63e3e8088 100644 --- a/API/DTOs/Update/UpdateNotificationDto.cs +++ b/API/DTOs/Update/UpdateNotificationDto.cs @@ -38,4 +38,16 @@ public class UpdateNotificationDto /// Date of the publish /// public required string PublishDate { get; init; } + /// + /// Is the server on a nightly within this release + /// + public bool IsOnNightlyInRelease { get; set; } + /// + /// Is the server on an older version + /// + public bool IsReleaseNewer { get; set; } + /// + /// Is the server on this version + /// + public bool IsReleaseEqual { get; set; } } diff --git a/API/DTOs/VolumeDto.cs b/API/DTOs/VolumeDto.cs index 8b1d49a7a..4820f4d95 100644 --- a/API/DTOs/VolumeDto.cs +++ b/API/DTOs/VolumeDto.cs @@ -9,11 +9,17 @@ namespace API.DTOs; public class VolumeDto : IHasReadTimeEstimate { public int Id { get; set; } - /// - public int Number { get; set; } - + /// + public float MinNumber { get; set; } + /// + public float MaxNumber { get; set; } /// public string Name { get; set; } = default!; + /// + /// This will map to MinNumber. Number was removed in v0.7.13.8/v0.7.14 + /// + [Obsolete("Use MinNumber")] + public float Number { get; set; } public int Pages { get; set; } public int PagesRead { get; set; } public DateTime LastModifiedUtc { get; set; } diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index c20e84d2a..6d37d95bc 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -58,6 +58,12 @@ public sealed class DataContext : IdentityDbContext AppUserDashboardStream { get; set; } = null!; public DbSet AppUserSideNavStream { get; set; } = null!; public DbSet AppUserExternalSource { get; set; } = null!; + public DbSet ExternalReview { get; set; } = null!; + public DbSet ExternalRating { get; set; } = null!; + public DbSet ExternalSeriesMetadata { get; set; } = null!; + public DbSet ExternalRecommendation { get; set; } = null!; + public DbSet ManualMigrationHistory { get; set; } = null!; + public DbSet SeriesBlacklist { get; set; } = null!; protected override void OnModelCreating(ModelBuilder builder) @@ -137,9 +143,15 @@ public sealed class DataContext : IdentityDbContext() .HasIndex(e => e.Visible) .IsUnique(false); + + builder.Entity() + .HasOne(em => em.Series) + .WithOne(s => s.ExternalSeriesMetadata) + .HasForeignKey(em => em.SeriesId) + .OnDelete(DeleteBehavior.Cascade); } - + #nullable enable private static void OnEntityTracked(object? sender, EntityTrackedEventArgs e) { if (e.FromQuery || e.Entry.State != EntityState.Added || e.Entry.Entity is not IEntityDate entity) return; @@ -156,6 +168,7 @@ public sealed class DataContext : IdentityDbContext +/// For the v0.7.14 release, one of the nightlies had bad data that would cause issues. This drops those records +/// +public static class MigrateClearNightlyExternalSeriesRecords +{ + public static async Task Migrate(DataContext dataContext, ILogger logger) + { + if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateClearNightlyExternalSeriesRecords")) + { + return; + } + + logger.LogCritical( + "Running MigrateClearNightlyExternalSeriesRecords migration - Please be patient, this may take some time. This is not an error"); + + dataContext.ExternalSeriesMetadata.RemoveRange(dataContext.ExternalSeriesMetadata); + dataContext.ExternalRating.RemoveRange(dataContext.ExternalRating); + dataContext.ExternalRecommendation.RemoveRange(dataContext.ExternalRecommendation); + dataContext.ExternalReview.RemoveRange(dataContext.ExternalReview); + + dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() + { + Name = "MigrateClearNightlyExternalSeriesRecords", + ProductVersion = BuildInfo.Version.ToString(), + RanAt = DateTime.UtcNow + }); + + await dataContext.SaveChangesAsync(); + + logger.LogCritical( + "Running MigrateClearNightlyExternalSeriesRecords migration - Completed. This is not an error"); + } +} diff --git a/API/Data/ManualMigrations/MigrateEmailTemplates.cs b/API/Data/ManualMigrations/MigrateEmailTemplates.cs new file mode 100644 index 000000000..ca0dc125b --- /dev/null +++ b/API/Data/ManualMigrations/MigrateEmailTemplates.cs @@ -0,0 +1,59 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using API.Services; +using Flurl.Http; +using Microsoft.Extensions.Logging; + +namespace API.Data.ManualMigrations; + +public static class MigrateEmailTemplates +{ + private const string EmailChange = "https://raw.githubusercontent.com/Kareadita/KavitaEmail/main/KavitaEmail/config/templates/EmailChange.html"; + private const string EmailConfirm = "https://raw.githubusercontent.com/Kareadita/KavitaEmail/main/KavitaEmail/config/templates/EmailConfirm.html"; + private const string EmailPasswordReset = "https://raw.githubusercontent.com/Kareadita/KavitaEmail/main/KavitaEmail/config/templates/EmailPasswordReset.html"; + private const string SendToDevice = "https://raw.githubusercontent.com/Kareadita/KavitaEmail/main/KavitaEmail/config/templates/SendToDevice.html"; + private const string EmailTest = "https://raw.githubusercontent.com/Kareadita/KavitaEmail/main/KavitaEmail/config/templates/EmailTest.html"; + + public static async Task Migrate(IDirectoryService directoryService, ILogger logger) + { + var files = directoryService.GetFiles(directoryService.CustomizedTemplateDirectory); + if (files.Any()) + { + logger.LogCritical("Running MigrateEmailTemplates migration - Completed. This is not an error"); + return; + } + + // Write files to directory + await DownloadAndWriteToFile(EmailChange, Path.Join(directoryService.CustomizedTemplateDirectory, "EmailChange.html"), logger); + await DownloadAndWriteToFile(EmailConfirm, Path.Join(directoryService.CustomizedTemplateDirectory, "EmailConfirm.html"), logger); + await DownloadAndWriteToFile(EmailPasswordReset, Path.Join(directoryService.CustomizedTemplateDirectory, "EmailPasswordReset.html"), logger); + await DownloadAndWriteToFile(SendToDevice, Path.Join(directoryService.CustomizedTemplateDirectory, "SendToDevice.html"), logger); + await DownloadAndWriteToFile(EmailTest, Path.Join(directoryService.CustomizedTemplateDirectory, "EmailTest.html"), logger); + + + + logger.LogCritical("Running MigrateEmailTemplates migration - Please be patient, this may take some time. This is not an error"); + } + + private static async Task DownloadAndWriteToFile(string url, string filePath, ILogger logger) + { + try + { + // Download the raw text using Flurl + var content = await url.GetStringAsync(); + + // Write the content to a file + await File.WriteAllTextAsync(filePath, content); + + logger.LogInformation("{File} downloaded and written successfully", filePath); + } + catch (FlurlHttpException ex) + { + logger.LogError(ex, "Unable to download {Url} to {FilePath}. Please perform yourself!", url, filePath); + } + } + + +} diff --git a/API/Data/ManualMigrations/MigrateLibrariesToHaveAllFileTypes.cs b/API/Data/ManualMigrations/MigrateLibrariesToHaveAllFileTypes.cs index 1da2f6303..dfd5ab954 100644 --- a/API/Data/ManualMigrations/MigrateLibrariesToHaveAllFileTypes.cs +++ b/API/Data/ManualMigrations/MigrateLibrariesToHaveAllFileTypes.cs @@ -15,9 +15,20 @@ public static class MigrateLibrariesToHaveAllFileTypes { public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger logger) { + if (await dataContext.Library.AnyAsync(l => l.LibraryFileTypes.Count == 0)) + { + logger.LogCritical("Running MigrateLibrariesToHaveAllFileTypes migration - Completed. This is not an error"); + return; + } + logger.LogCritical("Running MigrateLibrariesToHaveAllFileTypes migration - Please be patient, this may take some time. This is not an error"); - var allLibs = await dataContext.Library.Include(l => l.LibraryFileTypes).ToListAsync(); - foreach (var library in allLibs.Where(library => library.LibraryFileTypes.Count == 0)) + + var allLibs = await dataContext.Library + .Include(l => l.LibraryFileTypes) + .Where(library => library.LibraryFileTypes.Count == 0) + .ToListAsync(); + + foreach (var library in allLibs) { switch (library.Type) { @@ -57,11 +68,14 @@ public static class MigrateLibrariesToHaveAllFileTypes }); break; default: - throw new ArgumentOutOfRangeException(); + break; } } - await dataContext.SaveChangesAsync(); + if (unitOfWork.HasChanges()) + { + await dataContext.SaveChangesAsync(); + } logger.LogCritical("Running MigrateLibrariesToHaveAllFileTypes migration - Completed. This is not an error"); } } diff --git a/API/Data/ManualMigrations/MigrateManualHistory.cs b/API/Data/ManualMigrations/MigrateManualHistory.cs new file mode 100644 index 000000000..be41f0992 --- /dev/null +++ b/API/Data/ManualMigrations/MigrateManualHistory.cs @@ -0,0 +1,86 @@ +using System; +using System.Threading.Tasks; +using API.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace API.Data.ManualMigrations; + +/// +/// Introduced in v0.7.14, will store history so that going forward, migrations can just check against the history +/// and I don't need to remove old migrations +/// +public static class MigrateManualHistory +{ + public static async Task Migrate(DataContext dataContext, ILogger logger) + { + if (await dataContext.ManualMigrationHistory.AnyAsync()) + { + logger.LogCritical( + "Running MigrateManualHistory migration - Completed. This is not an error"); + return; + } + + logger.LogCritical( + "Running MigrateManualHistory migration - Please be patient, this may take some time. This is not an error"); + + dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() + { + Name = "MigrateUserLibrarySideNavStream", + ProductVersion = "0.7.9.0", + RanAt = DateTime.UtcNow + }); + + dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() + { + Name = "MigrateSmartFilterEncoding", + ProductVersion = "0.7.11.0", + RanAt = DateTime.UtcNow + }); + dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() + { + Name = "MigrateLibrariesToHaveAllFileTypes", + ProductVersion = "0.7.11.0", + RanAt = DateTime.UtcNow + }); + + dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() + { + Name = "MigrateEmailTemplates", + ProductVersion = "0.7.14.0", + RanAt = DateTime.UtcNow + }); + dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() + { + Name = "MigrateVolumeNumber", + ProductVersion = "0.7.14.0", + RanAt = DateTime.UtcNow + }); + + dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() + { + Name = "MigrateWantToReadExport", + ProductVersion = "0.7.14.0", + RanAt = DateTime.UtcNow + }); + + dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() + { + Name = "MigrateWantToReadImport", + ProductVersion = "0.7.14.0", + RanAt = DateTime.UtcNow + }); + + dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() + { + Name = "MigrateManualHistory", + ProductVersion = "0.7.14.0", + RanAt = DateTime.UtcNow + }); + + await dataContext.SaveChangesAsync(); + + logger.LogCritical( + "Running MigrateManualHistory migration - Completed. This is not an error"); + } +} diff --git a/API/Data/ManualMigrations/MigrateUserLibrarySideNavStream.cs b/API/Data/ManualMigrations/MigrateUserLibrarySideNavStream.cs index d4220e7f7..290bd0dc9 100644 --- a/API/Data/ManualMigrations/MigrateUserLibrarySideNavStream.cs +++ b/API/Data/ManualMigrations/MigrateUserLibrarySideNavStream.cs @@ -14,9 +14,9 @@ public static class MigrateUserLibrarySideNavStream { public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger logger) { - logger.LogCritical("Running MigrateUserLibrarySideNavStream migration - Please be patient, this may take some time. This is not an error"); - var usersWithLibraryStreams = await dataContext.AppUser.Include(u => u.SideNavStreams) + var usersWithLibraryStreams = await dataContext.AppUser + .Include(u => u.SideNavStreams) .AnyAsync(u => u.SideNavStreams.Count > 0 && u.SideNavStreams.Any(s => s.LibraryId > 0)); if (usersWithLibraryStreams) @@ -25,6 +25,8 @@ public static class MigrateUserLibrarySideNavStream return; } + logger.LogCritical("Running MigrateUserLibrarySideNavStream migration - Please be patient, this may take some time. This is not an error"); + var users = await unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.SideNavStreams); foreach (var user in users) { diff --git a/API/Data/ManualMigrations/MigrateVolumeNumber.cs b/API/Data/ManualMigrations/MigrateVolumeNumber.cs new file mode 100644 index 000000000..cae2e7f3c --- /dev/null +++ b/API/Data/ManualMigrations/MigrateVolumeNumber.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading.Tasks; +using API.Entities.Enums; +using API.Services.Tasks.Scanner.Parser; +using Kavita.Common.EnvironmentInfo; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace API.Data.ManualMigrations; + +/// +/// Introduced in v0.7.14, this migrates the existing Volume Name -> Volume Min/Max Number +/// +public static class MigrateVolumeNumber +{ + public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger logger) + { + if (await dataContext.Volume.AnyAsync(v => v.MaxNumber > 0)) + { + logger.LogCritical( + "Running MigrateVolumeNumber migration - Completed. This is not an error"); + return; + } + + logger.LogCritical( + "Running MigrateVolumeNumber migration - Please be patient, this may take some time. This is not an error"); + + // Get all volumes + foreach (var volume in dataContext.Volume) + { + volume.MinNumber = Parser.MinNumberFromRange(volume.Name); + volume.MaxNumber = Parser.MaxNumberFromRange(volume.Name); + } + + await dataContext.SaveChangesAsync(); + logger.LogCritical( + "Running MigrateVolumeNumber migration - Completed. This is not an error"); + } +} diff --git a/API/Data/ManualMigrations/MigrateWantToReadExport.cs b/API/Data/ManualMigrations/MigrateWantToReadExport.cs new file mode 100644 index 000000000..cff05b9a8 --- /dev/null +++ b/API/Data/ManualMigrations/MigrateWantToReadExport.cs @@ -0,0 +1,79 @@ +using System; +using System.Globalization; +using System.IO; +using System.Threading.Tasks; +using API.Services; +using CsvHelper; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace API.Data.ManualMigrations; + + +/// +/// v0.7.13.12/v0.7.14 - Want to read is extracted and saved in a csv +/// +/// This must run BEFORE any DB migrations +public static class MigrateWantToReadExport +{ + public static async Task Migrate(DataContext dataContext, IDirectoryService directoryService, ILogger logger) + { + try + { + var importFile = Path.Join(directoryService.ConfigDirectory, "want-to-read-migration.csv"); + if (File.Exists(importFile)) + { + logger.LogCritical( + "Running MigrateWantToReadExport migration - Completed. This is not an error"); + return; + } + + logger.LogCritical( + "Running MigrateWantToReadExport migration - Please be patient, this may take some time. This is not an error"); + + await using var command = dataContext.Database.GetDbConnection().CreateCommand(); + command.CommandText = "Select AppUserId, Id from Series WHERE AppUserId IS NOT NULL ORDER BY AppUserId;"; + + await dataContext.Database.OpenConnectionAsync(); + await using var result = await command.ExecuteReaderAsync(); + + await using var writer = + new StreamWriter(Path.Join(directoryService.ConfigDirectory, "want-to-read-migration.csv")); + await using var csvWriter = new CsvWriter(writer, CultureInfo.InvariantCulture); + + // Write header + csvWriter.WriteField("AppUserId"); + csvWriter.WriteField("Id"); + await csvWriter.NextRecordAsync(); + + // Write data + while (await result.ReadAsync()) + { + var appUserId = result["AppUserId"].ToString(); + var id = result["Id"].ToString(); + + csvWriter.WriteField(appUserId); + csvWriter.WriteField(id); + await csvWriter.NextRecordAsync(); + } + + + try + { + await dataContext.Database.CloseConnectionAsync(); + writer.Close(); + } + catch (Exception) + { + /* Swallow */ + } + + logger.LogCritical( + "Running MigrateWantToReadExport migration - Completed. This is not an error"); + } + catch (Exception ex) + { + // On new installs, the db isn't setup yet, so this has nothing to do + } + } +} diff --git a/API/Data/ManualMigrations/MigrateWantToReadImport.cs b/API/Data/ManualMigrations/MigrateWantToReadImport.cs new file mode 100644 index 000000000..01982e58f --- /dev/null +++ b/API/Data/ManualMigrations/MigrateWantToReadImport.cs @@ -0,0 +1,60 @@ +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using API.Data.Repositories; +using API.Entities; +using API.Services; +using CsvHelper; +using Microsoft.Extensions.Logging; + +namespace API.Data.ManualMigrations; + +/// +/// v0.7.13.12/v0.7.14 - Want to read is imported from a csv +/// +public static class MigrateWantToReadImport +{ + public static async Task Migrate(IUnitOfWork unitOfWork, IDirectoryService directoryService, ILogger logger) + { + var importFile = Path.Join(directoryService.ConfigDirectory, "want-to-read-migration.csv"); + var outputFile = Path.Join(directoryService.ConfigDirectory, "imported-want-to-read-migration.csv"); + + if (!File.Exists(importFile) || File.Exists(outputFile)) + { + logger.LogCritical( + "Running MigrateWantToReadImport migration - Completed. This is not an error"); + return; + } + + logger.LogCritical( + "Running MigrateWantToReadImport migration - Please be patient, this may take some time. This is not an error"); + + using var reader = new StreamReader(importFile); + using var csvReader = new CsvReader(reader, CultureInfo.InvariantCulture); + // Read the records from the CSV file + await csvReader.ReadAsync(); + csvReader.ReadHeader(); // Skip the header row + + while (await csvReader.ReadAsync()) + { + // Read the values of AppUserId and Id columns + var appUserId = csvReader.GetField("AppUserId"); + var seriesId = csvReader.GetField("Id"); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(appUserId, AppUserIncludes.WantToRead); + if (user == null || user.WantToRead.Any(w => w.SeriesId == seriesId)) continue; + + user.WantToRead.Add(new AppUserWantToRead() + { + SeriesId = seriesId + }); + } + + await unitOfWork.CommitAsync(); + reader.Close(); + + File.WriteAllLines(outputFile, await File.ReadAllLinesAsync(importFile)); + logger.LogCritical( + "Running MigrateWantToReadImport migration - Completed. This is not an error"); + } +} diff --git a/API/Data/Migrations/20240121223643_ExternalSeriesMetadata.Designer.cs b/API/Data/Migrations/20240121223643_ExternalSeriesMetadata.Designer.cs new file mode 100644 index 000000000..e7fdad65e --- /dev/null +++ b/API/Data/Migrations/20240121223643_ExternalSeriesMetadata.Designer.cs @@ -0,0 +1,2787 @@ +// +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("20240121223643_ExternalSeriesMetadata")] + partial class ExternalSeriesMetadata + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("AgeRestriction") + .HasColumnType("INTEGER"); + + b.Property("AgeRestrictionIncludeUnknowns") + .HasColumnType("INTEGER"); + + b.Property("AniListAccessToken") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("ConfirmationToken") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LastActiveUtc") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Page") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserBookmark"); + }); + + modelBuilder.Entity("API.Entities.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("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("PromptForDownloadSize") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("ShareReviews") + .HasColumnType("INTEGER"); + + b.Property("ShowScreenHints") + .HasColumnType("INTEGER"); + + b.Property("SwipeToPaginate") + .HasColumnType("INTEGER"); + + b.Property("ThemeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.HasIndex("ThemeId"); + + b.ToTable("AppUserPreferences"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("HasBeenRated") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSourceId") + .HasColumnType("INTEGER"); + + b.Property("IsProvided") + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("SmartFilterId") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(5); + + b.Property("Visible") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SmartFilterId"); + + b.HasIndex("Visible"); + + b.ToTable("AppUserSideNavStream"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Filter") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserSmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PageNumber") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserTableOfContent"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AlternateCount") + .HasColumnType("INTEGER"); + + b.Property("AlternateNumber") + .HasColumnType("TEXT"); + + b.Property("AlternateSeries") + .HasColumnType("TEXT"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("ISBN") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SeriesGroup") + .HasColumnType("TEXT"); + + b.Property("StoryArc") + .HasColumnType("TEXT"); + + b.Property("StoryArcNumber") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TitleName") + .HasColumnType("TEXT"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("API.Entities.CollectionTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Promoted") + .IsUnique(); + + b.ToTable("CollectionTag"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastUsed") + .HasColumnType("TEXT"); + + b.Property("LastUsedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Platform") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("Device"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowScrobbling") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderWatching") + .HasColumnType("INTEGER"); + + b.Property("IncludeInDashboard") + .HasColumnType("INTEGER"); + + b.Property("IncludeInRecommended") + .HasColumnType("INTEGER"); + + b.Property("IncludeInSearch") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("ManageCollections") + .HasColumnType("INTEGER"); + + b.Property("ManageReadingLists") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.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("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastFileAnalysis") + .HasColumnType("TEXT"); + + b.Property("LastFileAnalysisUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.MediaError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MediaError"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AverageScore") + .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.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("Body") + .HasColumnType("TEXT"); + + b.Property("BodyJustText") + .HasColumnType("TEXT"); + + 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.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("GoogleBooksId") + .HasColumnType("TEXT"); + + b.Property("LastUpdatedUtc") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.ToTable("ExternalSeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AgeRatingLocked") + .HasColumnType("INTEGER"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("CoverArtistLocked") + .HasColumnType("INTEGER"); + + b.Property("EditorLocked") + .HasColumnType("INTEGER"); + + b.Property("GenresLocked") + .HasColumnType("INTEGER"); + + b.Property("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxCount") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatusLocked") + .HasColumnType("INTEGER"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYear") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYearLocked") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.HasIndex("Id", "SeriesId") + .IsUnique(); + + b.ToTable("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RelationKind") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("TargetSeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.HasIndex("TargetSeriesId"); + + b.ToTable("SeriesRelation"); + }); + + modelBuilder.Entity("API.Entities.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EndingMonth") + .HasColumnType("INTEGER"); + + b.Property("EndingYear") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("StartingMonth") + .HasColumnType("INTEGER"); + + b.Property("StartingYear") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("ReadingList"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ReadingListId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.HasIndex("ReadingListId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("VolumeId"); + + b.ToTable("ReadingListItem"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId1") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ScrobbleEventId1"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleError"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterNumber") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("IsProcessed") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("ProcessDateUtc") + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("ReviewBody") + .HasColumnType("TEXT"); + + b.Property("ReviewTitle") + .HasColumnType("TEXT"); + + b.Property("ScrobbleEventType") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeNumber") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("LibraryId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleEvent"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleHold"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderPath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastChapterAdded") + .HasColumnType("TEXT"); + + b.Property("LastChapterAddedUtc") + .HasColumnType("TEXT"); + + b.Property("LastFolderScanned") + .HasColumnType("TEXT"); + + b.Property("LastFolderScannedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("LocalizedNameLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedLocalizedName") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("SortNameLocked") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.ServerSetting", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("ServerSetting"); + }); + + modelBuilder.Entity("API.Entities.ServerStatistics", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterCount") + .HasColumnType("INTEGER"); + + b.Property("FileCount") + .HasColumnType("INTEGER"); + + b.Property("GenreCount") + .HasColumnType("INTEGER"); + + b.Property("PersonCount") + .HasColumnType("INTEGER"); + + b.Property("SeriesCount") + .HasColumnType("INTEGER"); + + b.Property("TagCount") + .HasColumnType("INTEGER"); + + b.Property("UserCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeCount") + .HasColumnType("INTEGER"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ServerStatistics"); + }); + + modelBuilder.Entity("API.Entities.SiteTheme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SiteTheme"); + }); + + modelBuilder.Entity("API.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "GenresId"); + + b.HasIndex("GenresId"); + + b.ToTable("ChapterGenre"); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.Property("ChapterMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterMetadatasId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.ToTable("ChapterPerson"); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("ChapterTag"); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.Property("CollectionTagsId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionTagsId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("CollectionTagSeriesMetadata"); + }); + + modelBuilder.Entity("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("PersonSeriesMetadata", b => + { + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("PeopleId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("PersonSeriesMetadata"); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("SeriesMetadatasId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("SeriesMetadataTag"); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("DashboardStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserExternalSource", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ExternalSources") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.SiteTheme", "Theme") + .WithMany() + .HasForeignKey("ThemeId"); + + b.Navigation("AppUser"); + + b.Navigation("Theme"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", null) + .WithMany("UserProgress") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Progress") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SideNavStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SmartFilters") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("TableOfContents") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Devices") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.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.ExternalRecommendation", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId"); + + b.Navigation("Series"); + }); + + 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.SeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("Metadata") + .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Relations") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "TargetSeries") + .WithMany("RelationOf") + .HasForeignKey("TargetSeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + + b.Navigation("TargetSeries"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ReadingLists") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.ReadingList", "ReadingList") + .WithMany("Items") + .HasForeignKey("ReadingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Volume", "Volume") + .WithMany() + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("ReadingList"); + + b.Navigation("Series"); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") + .WithMany() + .HasForeignKey("ScrobbleEventId1"); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ScrobbleEvent"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", "Library") + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Library"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ScrobbleHolds") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany("WantToRead") + .HasForeignKey("AppUserId"); + + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChapterMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.HasOne("API.Entities.CollectionTag", null) + .WithMany() + .HasForeignKey("CollectionTagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("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("PersonSeriesMetadata", b => + { + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Bookmarks"); + + b.Navigation("DashboardStreams"); + + b.Navigation("Devices"); + + b.Navigation("ExternalSources"); + + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("ReadingLists"); + + b.Navigation("ScrobbleHolds"); + + b.Navigation("SideNavStreams"); + + b.Navigation("SmartFilters"); + + b.Navigation("TableOfContents"); + + b.Navigation("UserPreferences"); + + b.Navigation("UserRoles"); + + b.Navigation("WantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("Files"); + + 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.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/20240121223643_ExternalSeriesMetadata.cs b/API/Data/Migrations/20240121223643_ExternalSeriesMetadata.cs new file mode 100644 index 000000000..718332b9f --- /dev/null +++ b/API/Data/Migrations/20240121223643_ExternalSeriesMetadata.cs @@ -0,0 +1,227 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + /// + public partial class ExternalSeriesMetadata : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ExternalRating", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + AverageScore = table.Column(type: "INTEGER", nullable: false), + FavoriteCount = table.Column(type: "INTEGER", nullable: false), + Provider = table.Column(type: "INTEGER", nullable: false), + ProviderUrl = table.Column(type: "TEXT", nullable: true), + SeriesId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ExternalRating", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ExternalRecommendation", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", nullable: true), + CoverUrl = table.Column(type: "TEXT", nullable: true), + Url = table.Column(type: "TEXT", nullable: true), + Summary = table.Column(type: "TEXT", nullable: true), + AniListId = table.Column(type: "INTEGER", nullable: true), + MalId = table.Column(type: "INTEGER", nullable: true), + Provider = table.Column(type: "INTEGER", nullable: false), + SeriesId = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ExternalRecommendation", x => x.Id); + table.ForeignKey( + name: "FK_ExternalRecommendation_Series_SeriesId", + column: x => x.SeriesId, + principalTable: "Series", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "ExternalReview", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Tagline = table.Column(type: "TEXT", nullable: true), + Body = table.Column(type: "TEXT", nullable: true), + BodyJustText = table.Column(type: "TEXT", nullable: true), + RawBody = table.Column(type: "TEXT", nullable: true), + Provider = table.Column(type: "INTEGER", nullable: false), + SiteUrl = table.Column(type: "TEXT", nullable: true), + Username = table.Column(type: "TEXT", nullable: true), + Rating = table.Column(type: "INTEGER", nullable: false), + Score = table.Column(type: "INTEGER", nullable: false), + TotalVotes = table.Column(type: "INTEGER", nullable: false), + SeriesId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ExternalReview", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ExternalSeriesMetadata", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + AverageExternalRating = table.Column(type: "INTEGER", nullable: false), + AniListId = table.Column(type: "INTEGER", nullable: false), + MalId = table.Column(type: "INTEGER", nullable: false), + GoogleBooksId = table.Column(type: "TEXT", nullable: true), + LastUpdatedUtc = table.Column(type: "TEXT", nullable: false), + SeriesId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ExternalSeriesMetadata", x => x.Id); + table.ForeignKey( + name: "FK_ExternalSeriesMetadata_Series_SeriesId", + column: x => x.SeriesId, + principalTable: "Series", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ExternalRatingExternalSeriesMetadata", + columns: table => new + { + ExternalRatingsId = table.Column(type: "INTEGER", nullable: false), + ExternalSeriesMetadatasId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ExternalRatingExternalSeriesMetadata", x => new { x.ExternalRatingsId, x.ExternalSeriesMetadatasId }); + table.ForeignKey( + name: "FK_ExternalRatingExternalSeriesMetadata_ExternalRating_ExternalRatingsId", + column: x => x.ExternalRatingsId, + principalTable: "ExternalRating", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ExternalRatingExternalSeriesMetadata_ExternalSeriesMetadata_ExternalSeriesMetadatasId", + column: x => x.ExternalSeriesMetadatasId, + principalTable: "ExternalSeriesMetadata", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ExternalRecommendationExternalSeriesMetadata", + columns: table => new + { + ExternalRecommendationsId = table.Column(type: "INTEGER", nullable: false), + ExternalSeriesMetadatasId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ExternalRecommendationExternalSeriesMetadata", x => new { x.ExternalRecommendationsId, x.ExternalSeriesMetadatasId }); + table.ForeignKey( + name: "FK_ExternalRecommendationExternalSeriesMetadata_ExternalRecommendation_ExternalRecommendationsId", + column: x => x.ExternalRecommendationsId, + principalTable: "ExternalRecommendation", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ExternalRecommendationExternalSeriesMetadata_ExternalSeriesMetadata_ExternalSeriesMetadatasId", + column: x => x.ExternalSeriesMetadatasId, + principalTable: "ExternalSeriesMetadata", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ExternalReviewExternalSeriesMetadata", + columns: table => new + { + ExternalReviewsId = table.Column(type: "INTEGER", nullable: false), + ExternalSeriesMetadatasId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ExternalReviewExternalSeriesMetadata", x => new { x.ExternalReviewsId, x.ExternalSeriesMetadatasId }); + table.ForeignKey( + name: "FK_ExternalReviewExternalSeriesMetadata_ExternalReview_ExternalReviewsId", + column: x => x.ExternalReviewsId, + principalTable: "ExternalReview", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ExternalReviewExternalSeriesMetadata_ExternalSeriesMetadata_ExternalSeriesMetadatasId", + column: x => x.ExternalSeriesMetadatasId, + principalTable: "ExternalSeriesMetadata", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ExternalRatingExternalSeriesMetadata_ExternalSeriesMetadatasId", + table: "ExternalRatingExternalSeriesMetadata", + column: "ExternalSeriesMetadatasId"); + + migrationBuilder.CreateIndex( + name: "IX_ExternalRecommendation_SeriesId", + table: "ExternalRecommendation", + column: "SeriesId"); + + migrationBuilder.CreateIndex( + name: "IX_ExternalRecommendationExternalSeriesMetadata_ExternalSeriesMetadatasId", + table: "ExternalRecommendationExternalSeriesMetadata", + column: "ExternalSeriesMetadatasId"); + + migrationBuilder.CreateIndex( + name: "IX_ExternalReviewExternalSeriesMetadata_ExternalSeriesMetadatasId", + table: "ExternalReviewExternalSeriesMetadata", + column: "ExternalSeriesMetadatasId"); + + migrationBuilder.CreateIndex( + name: "IX_ExternalSeriesMetadata_SeriesId", + table: "ExternalSeriesMetadata", + column: "SeriesId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ExternalRatingExternalSeriesMetadata"); + + migrationBuilder.DropTable( + name: "ExternalRecommendationExternalSeriesMetadata"); + + migrationBuilder.DropTable( + name: "ExternalReviewExternalSeriesMetadata"); + + migrationBuilder.DropTable( + name: "ExternalRating"); + + migrationBuilder.DropTable( + name: "ExternalRecommendation"); + + migrationBuilder.DropTable( + name: "ExternalReview"); + + migrationBuilder.DropTable( + name: "ExternalSeriesMetadata"); + } + } +} diff --git a/API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.Designer.cs b/API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.Designer.cs new file mode 100644 index 000000000..730b40ec0 --- /dev/null +++ b/API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.Designer.cs @@ -0,0 +1,2793 @@ +// +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("20240128153433_VolumeMinMaxNumbers")] + partial class VolumeMinMaxNumbers + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("AgeRestriction") + .HasColumnType("INTEGER"); + + b.Property("AgeRestrictionIncludeUnknowns") + .HasColumnType("INTEGER"); + + b.Property("AniListAccessToken") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("ConfirmationToken") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LastActiveUtc") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Page") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserBookmark"); + }); + + modelBuilder.Entity("API.Entities.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("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("PromptForDownloadSize") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("ShareReviews") + .HasColumnType("INTEGER"); + + b.Property("ShowScreenHints") + .HasColumnType("INTEGER"); + + b.Property("SwipeToPaginate") + .HasColumnType("INTEGER"); + + b.Property("ThemeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.HasIndex("ThemeId"); + + b.ToTable("AppUserPreferences"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("HasBeenRated") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSourceId") + .HasColumnType("INTEGER"); + + b.Property("IsProvided") + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("SmartFilterId") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(5); + + b.Property("Visible") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SmartFilterId"); + + b.HasIndex("Visible"); + + b.ToTable("AppUserSideNavStream"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Filter") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserSmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PageNumber") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserTableOfContent"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AlternateCount") + .HasColumnType("INTEGER"); + + b.Property("AlternateNumber") + .HasColumnType("TEXT"); + + b.Property("AlternateSeries") + .HasColumnType("TEXT"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("ISBN") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SeriesGroup") + .HasColumnType("TEXT"); + + b.Property("StoryArc") + .HasColumnType("TEXT"); + + b.Property("StoryArcNumber") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TitleName") + .HasColumnType("TEXT"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("API.Entities.CollectionTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Promoted") + .IsUnique(); + + b.ToTable("CollectionTag"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastUsed") + .HasColumnType("TEXT"); + + b.Property("LastUsedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Platform") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("Device"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowScrobbling") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderWatching") + .HasColumnType("INTEGER"); + + b.Property("IncludeInDashboard") + .HasColumnType("INTEGER"); + + b.Property("IncludeInRecommended") + .HasColumnType("INTEGER"); + + b.Property("IncludeInSearch") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("ManageCollections") + .HasColumnType("INTEGER"); + + b.Property("ManageReadingLists") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.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("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastFileAnalysis") + .HasColumnType("TEXT"); + + b.Property("LastFileAnalysisUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.MediaError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MediaError"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AverageScore") + .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.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("Body") + .HasColumnType("TEXT"); + + b.Property("BodyJustText") + .HasColumnType("TEXT"); + + 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.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("GoogleBooksId") + .HasColumnType("TEXT"); + + b.Property("LastUpdatedUtc") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.ToTable("ExternalSeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AgeRatingLocked") + .HasColumnType("INTEGER"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("CoverArtistLocked") + .HasColumnType("INTEGER"); + + b.Property("EditorLocked") + .HasColumnType("INTEGER"); + + b.Property("GenresLocked") + .HasColumnType("INTEGER"); + + b.Property("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxCount") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatusLocked") + .HasColumnType("INTEGER"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYear") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYearLocked") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.HasIndex("Id", "SeriesId") + .IsUnique(); + + b.ToTable("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RelationKind") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("TargetSeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.HasIndex("TargetSeriesId"); + + b.ToTable("SeriesRelation"); + }); + + modelBuilder.Entity("API.Entities.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EndingMonth") + .HasColumnType("INTEGER"); + + b.Property("EndingYear") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("StartingMonth") + .HasColumnType("INTEGER"); + + b.Property("StartingYear") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("ReadingList"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ReadingListId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.HasIndex("ReadingListId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("VolumeId"); + + b.ToTable("ReadingListItem"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId1") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ScrobbleEventId1"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleError"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterNumber") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("IsProcessed") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("ProcessDateUtc") + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("ReviewBody") + .HasColumnType("TEXT"); + + b.Property("ReviewTitle") + .HasColumnType("TEXT"); + + b.Property("ScrobbleEventType") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeNumber") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("LibraryId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleEvent"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleHold"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderPath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastChapterAdded") + .HasColumnType("TEXT"); + + b.Property("LastChapterAddedUtc") + .HasColumnType("TEXT"); + + b.Property("LastFolderScanned") + .HasColumnType("TEXT"); + + b.Property("LastFolderScannedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("LocalizedNameLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedLocalizedName") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("SortNameLocked") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.ServerSetting", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("ServerSetting"); + }); + + modelBuilder.Entity("API.Entities.ServerStatistics", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterCount") + .HasColumnType("INTEGER"); + + b.Property("FileCount") + .HasColumnType("INTEGER"); + + b.Property("GenreCount") + .HasColumnType("INTEGER"); + + b.Property("PersonCount") + .HasColumnType("INTEGER"); + + b.Property("SeriesCount") + .HasColumnType("INTEGER"); + + b.Property("TagCount") + .HasColumnType("INTEGER"); + + b.Property("UserCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeCount") + .HasColumnType("INTEGER"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ServerStatistics"); + }); + + modelBuilder.Entity("API.Entities.SiteTheme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SiteTheme"); + }); + + modelBuilder.Entity("API.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("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("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "GenresId"); + + b.HasIndex("GenresId"); + + b.ToTable("ChapterGenre"); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.Property("ChapterMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterMetadatasId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.ToTable("ChapterPerson"); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("ChapterTag"); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.Property("CollectionTagsId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionTagsId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("CollectionTagSeriesMetadata"); + }); + + modelBuilder.Entity("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("PersonSeriesMetadata", b => + { + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("PeopleId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("PersonSeriesMetadata"); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("SeriesMetadatasId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("SeriesMetadataTag"); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("DashboardStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserExternalSource", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ExternalSources") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.SiteTheme", "Theme") + .WithMany() + .HasForeignKey("ThemeId"); + + b.Navigation("AppUser"); + + b.Navigation("Theme"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", null) + .WithMany("UserProgress") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Progress") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SideNavStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SmartFilters") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("TableOfContents") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Devices") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.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.ExternalRecommendation", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId"); + + b.Navigation("Series"); + }); + + 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.SeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("Metadata") + .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Relations") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "TargetSeries") + .WithMany("RelationOf") + .HasForeignKey("TargetSeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + + b.Navigation("TargetSeries"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ReadingLists") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.ReadingList", "ReadingList") + .WithMany("Items") + .HasForeignKey("ReadingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Volume", "Volume") + .WithMany() + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("ReadingList"); + + b.Navigation("Series"); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") + .WithMany() + .HasForeignKey("ScrobbleEventId1"); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ScrobbleEvent"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", "Library") + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Library"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ScrobbleHolds") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany("WantToRead") + .HasForeignKey("AppUserId"); + + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChapterMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.HasOne("API.Entities.CollectionTag", null) + .WithMany() + .HasForeignKey("CollectionTagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("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("PersonSeriesMetadata", b => + { + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Bookmarks"); + + b.Navigation("DashboardStreams"); + + b.Navigation("Devices"); + + b.Navigation("ExternalSources"); + + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("ReadingLists"); + + b.Navigation("ScrobbleHolds"); + + b.Navigation("SideNavStreams"); + + b.Navigation("SmartFilters"); + + b.Navigation("TableOfContents"); + + b.Navigation("UserPreferences"); + + b.Navigation("UserRoles"); + + b.Navigation("WantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("Files"); + + 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.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/20240128153433_VolumeMinMaxNumbers.cs b/API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.cs new file mode 100644 index 000000000..491fd057f --- /dev/null +++ b/API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + /// + public partial class VolumeMinMaxNumbers : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "MaxNumber", + table: "Volume", + type: "REAL", + nullable: false, + defaultValue: 0f); + + migrationBuilder.AddColumn( + name: "MinNumber", + table: "Volume", + type: "REAL", + nullable: false, + defaultValue: 0f); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "MaxNumber", + table: "Volume"); + + migrationBuilder.DropColumn( + name: "MinNumber", + table: "Volume"); + } + } +} diff --git a/API/Data/Migrations/20240130190617_WantToReadFix.Designer.cs b/API/Data/Migrations/20240130190617_WantToReadFix.Designer.cs new file mode 100644 index 000000000..a4203171c --- /dev/null +++ b/API/Data/Migrations/20240130190617_WantToReadFix.Designer.cs @@ -0,0 +1,2844 @@ +// +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("20240130190617_WantToReadFix")] + partial class WantToReadFix + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("AgeRestriction") + .HasColumnType("INTEGER"); + + b.Property("AgeRestrictionIncludeUnknowns") + .HasColumnType("INTEGER"); + + b.Property("AniListAccessToken") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("ConfirmationToken") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LastActiveUtc") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Page") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserBookmark"); + }); + + modelBuilder.Entity("API.Entities.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("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("PromptForDownloadSize") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("ShareReviews") + .HasColumnType("INTEGER"); + + b.Property("ShowScreenHints") + .HasColumnType("INTEGER"); + + b.Property("SwipeToPaginate") + .HasColumnType("INTEGER"); + + b.Property("ThemeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.HasIndex("ThemeId"); + + b.ToTable("AppUserPreferences"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("HasBeenRated") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSourceId") + .HasColumnType("INTEGER"); + + b.Property("IsProvided") + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("SmartFilterId") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(5); + + b.Property("Visible") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SmartFilterId"); + + b.HasIndex("Visible"); + + b.ToTable("AppUserSideNavStream"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Filter") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserSmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PageNumber") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserTableOfContent"); + }); + + modelBuilder.Entity("API.Entities.AppUserWantToRead", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserWantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AlternateCount") + .HasColumnType("INTEGER"); + + b.Property("AlternateNumber") + .HasColumnType("TEXT"); + + b.Property("AlternateSeries") + .HasColumnType("TEXT"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("ISBN") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SeriesGroup") + .HasColumnType("TEXT"); + + b.Property("StoryArc") + .HasColumnType("TEXT"); + + b.Property("StoryArcNumber") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TitleName") + .HasColumnType("TEXT"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("API.Entities.CollectionTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Promoted") + .IsUnique(); + + b.ToTable("CollectionTag"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastUsed") + .HasColumnType("TEXT"); + + b.Property("LastUsedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Platform") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("Device"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowScrobbling") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderWatching") + .HasColumnType("INTEGER"); + + b.Property("IncludeInDashboard") + .HasColumnType("INTEGER"); + + b.Property("IncludeInRecommended") + .HasColumnType("INTEGER"); + + b.Property("IncludeInSearch") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("ManageCollections") + .HasColumnType("INTEGER"); + + b.Property("ManageReadingLists") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.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("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastFileAnalysis") + .HasColumnType("TEXT"); + + b.Property("LastFileAnalysisUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.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.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("AverageScore") + .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.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("Body") + .HasColumnType("TEXT"); + + b.Property("BodyJustText") + .HasColumnType("TEXT"); + + 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.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("GoogleBooksId") + .HasColumnType("TEXT"); + + b.Property("LastUpdatedUtc") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.ToTable("ExternalSeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AgeRatingLocked") + .HasColumnType("INTEGER"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("CoverArtistLocked") + .HasColumnType("INTEGER"); + + b.Property("EditorLocked") + .HasColumnType("INTEGER"); + + b.Property("GenresLocked") + .HasColumnType("INTEGER"); + + b.Property("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxCount") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatusLocked") + .HasColumnType("INTEGER"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYear") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYearLocked") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.HasIndex("Id", "SeriesId") + .IsUnique(); + + b.ToTable("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RelationKind") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("TargetSeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.HasIndex("TargetSeriesId"); + + b.ToTable("SeriesRelation"); + }); + + modelBuilder.Entity("API.Entities.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EndingMonth") + .HasColumnType("INTEGER"); + + b.Property("EndingYear") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("StartingMonth") + .HasColumnType("INTEGER"); + + b.Property("StartingYear") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("ReadingList"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ReadingListId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.HasIndex("ReadingListId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("VolumeId"); + + b.ToTable("ReadingListItem"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId1") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ScrobbleEventId1"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleError"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterNumber") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("IsProcessed") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("ProcessDateUtc") + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("ReviewBody") + .HasColumnType("TEXT"); + + b.Property("ReviewTitle") + .HasColumnType("TEXT"); + + b.Property("ScrobbleEventType") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeNumber") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("LibraryId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleEvent"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleHold"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderPath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastChapterAdded") + .HasColumnType("TEXT"); + + b.Property("LastChapterAddedUtc") + .HasColumnType("TEXT"); + + b.Property("LastFolderScanned") + .HasColumnType("TEXT"); + + b.Property("LastFolderScannedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("LocalizedNameLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedLocalizedName") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("SortNameLocked") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.ServerSetting", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("ServerSetting"); + }); + + modelBuilder.Entity("API.Entities.ServerStatistics", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterCount") + .HasColumnType("INTEGER"); + + b.Property("FileCount") + .HasColumnType("INTEGER"); + + b.Property("GenreCount") + .HasColumnType("INTEGER"); + + b.Property("PersonCount") + .HasColumnType("INTEGER"); + + b.Property("SeriesCount") + .HasColumnType("INTEGER"); + + b.Property("TagCount") + .HasColumnType("INTEGER"); + + b.Property("UserCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeCount") + .HasColumnType("INTEGER"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ServerStatistics"); + }); + + modelBuilder.Entity("API.Entities.SiteTheme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SiteTheme"); + }); + + modelBuilder.Entity("API.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("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("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "GenresId"); + + b.HasIndex("GenresId"); + + b.ToTable("ChapterGenre"); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.Property("ChapterMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterMetadatasId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.ToTable("ChapterPerson"); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("ChapterTag"); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.Property("CollectionTagsId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionTagsId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("CollectionTagSeriesMetadata"); + }); + + modelBuilder.Entity("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("PersonSeriesMetadata", b => + { + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("PeopleId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("PersonSeriesMetadata"); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("SeriesMetadatasId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("SeriesMetadataTag"); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("DashboardStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserExternalSource", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ExternalSources") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.SiteTheme", "Theme") + .WithMany() + .HasForeignKey("ThemeId"); + + b.Navigation("AppUser"); + + b.Navigation("Theme"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", null) + .WithMany("UserProgress") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Progress") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SideNavStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SmartFilters") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("TableOfContents") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserWantToRead", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("WantToRead") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Devices") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.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.ExternalRecommendation", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId"); + + b.Navigation("Series"); + }); + + 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.SeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("Metadata") + .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Relations") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "TargetSeries") + .WithMany("RelationOf") + .HasForeignKey("TargetSeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + + b.Navigation("TargetSeries"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ReadingLists") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.ReadingList", "ReadingList") + .WithMany("Items") + .HasForeignKey("ReadingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Volume", "Volume") + .WithMany() + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("ReadingList"); + + b.Navigation("Series"); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") + .WithMany() + .HasForeignKey("ScrobbleEventId1"); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ScrobbleEvent"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", "Library") + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Library"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ScrobbleHolds") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChapterMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.HasOne("API.Entities.CollectionTag", null) + .WithMany() + .HasForeignKey("CollectionTagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("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("PersonSeriesMetadata", b => + { + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Bookmarks"); + + b.Navigation("DashboardStreams"); + + b.Navigation("Devices"); + + b.Navigation("ExternalSources"); + + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("ReadingLists"); + + b.Navigation("ScrobbleHolds"); + + b.Navigation("SideNavStreams"); + + b.Navigation("SmartFilters"); + + b.Navigation("TableOfContents"); + + b.Navigation("UserPreferences"); + + b.Navigation("UserRoles"); + + b.Navigation("WantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("Files"); + + 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.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/20240130190617_WantToReadFix.cs b/API/Data/Migrations/20240130190617_WantToReadFix.cs new file mode 100644 index 000000000..386160db3 --- /dev/null +++ b/API/Data/Migrations/20240130190617_WantToReadFix.cs @@ -0,0 +1,106 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + /// + public partial class WantToReadFix : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Series_AspNetUsers_AppUserId", + table: "Series"); + + migrationBuilder.DropIndex( + name: "IX_Series_AppUserId", + table: "Series"); + + migrationBuilder.DropColumn( + name: "AppUserId", + table: "Series"); + + migrationBuilder.CreateTable( + name: "AppUserWantToRead", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + SeriesId = table.Column(type: "INTEGER", nullable: false), + AppUserId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AppUserWantToRead", x => x.Id); + table.ForeignKey( + name: "FK_AppUserWantToRead_AspNetUsers_AppUserId", + column: x => x.AppUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AppUserWantToRead_Series_SeriesId", + column: x => x.SeriesId, + principalTable: "Series", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ManualMigrationHistory", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ProductVersion = table.Column(type: "TEXT", nullable: true), + Name = table.Column(type: "TEXT", nullable: true), + RanAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ManualMigrationHistory", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_AppUserWantToRead_AppUserId", + table: "AppUserWantToRead", + column: "AppUserId"); + + migrationBuilder.CreateIndex( + name: "IX_AppUserWantToRead_SeriesId", + table: "AppUserWantToRead", + column: "SeriesId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AppUserWantToRead"); + + migrationBuilder.DropTable( + name: "ManualMigrationHistory"); + + migrationBuilder.AddColumn( + name: "AppUserId", + table: "Series", + type: "INTEGER", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Series_AppUserId", + table: "Series", + column: "AppUserId"); + + migrationBuilder.AddForeignKey( + name: "FK_Series_AspNetUsers_AppUserId", + table: "Series", + column: "AppUserId", + principalTable: "AspNetUsers", + principalColumn: "Id"); + } + } +} diff --git a/API/Data/Migrations/20240204141206_BlackListSeries.Designer.cs b/API/Data/Migrations/20240204141206_BlackListSeries.Designer.cs new file mode 100644 index 000000000..c399f13cc --- /dev/null +++ b/API/Data/Migrations/20240204141206_BlackListSeries.Designer.cs @@ -0,0 +1,2874 @@ +// +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("20240204141206_BlackListSeries")] + partial class BlackListSeries + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("AgeRestriction") + .HasColumnType("INTEGER"); + + b.Property("AgeRestrictionIncludeUnknowns") + .HasColumnType("INTEGER"); + + b.Property("AniListAccessToken") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("ConfirmationToken") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LastActiveUtc") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Page") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserBookmark"); + }); + + modelBuilder.Entity("API.Entities.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("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("PromptForDownloadSize") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("ShareReviews") + .HasColumnType("INTEGER"); + + b.Property("ShowScreenHints") + .HasColumnType("INTEGER"); + + b.Property("SwipeToPaginate") + .HasColumnType("INTEGER"); + + b.Property("ThemeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.HasIndex("ThemeId"); + + b.ToTable("AppUserPreferences"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("HasBeenRated") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSourceId") + .HasColumnType("INTEGER"); + + b.Property("IsProvided") + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("SmartFilterId") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(5); + + b.Property("Visible") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SmartFilterId"); + + b.HasIndex("Visible"); + + b.ToTable("AppUserSideNavStream"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Filter") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserSmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PageNumber") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserTableOfContent"); + }); + + modelBuilder.Entity("API.Entities.AppUserWantToRead", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserWantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AlternateCount") + .HasColumnType("INTEGER"); + + b.Property("AlternateNumber") + .HasColumnType("TEXT"); + + b.Property("AlternateSeries") + .HasColumnType("TEXT"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("ISBN") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SeriesGroup") + .HasColumnType("TEXT"); + + b.Property("StoryArc") + .HasColumnType("TEXT"); + + b.Property("StoryArcNumber") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TitleName") + .HasColumnType("TEXT"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("API.Entities.CollectionTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Promoted") + .IsUnique(); + + b.ToTable("CollectionTag"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastUsed") + .HasColumnType("TEXT"); + + b.Property("LastUsedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Platform") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("Device"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowScrobbling") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderWatching") + .HasColumnType("INTEGER"); + + b.Property("IncludeInDashboard") + .HasColumnType("INTEGER"); + + b.Property("IncludeInRecommended") + .HasColumnType("INTEGER"); + + b.Property("IncludeInSearch") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("ManageCollections") + .HasColumnType("INTEGER"); + + b.Property("ManageReadingLists") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.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("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastFileAnalysis") + .HasColumnType("TEXT"); + + b.Property("LastFileAnalysisUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.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.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("AverageScore") + .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.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("Body") + .HasColumnType("TEXT"); + + b.Property("BodyJustText") + .HasColumnType("TEXT"); + + 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.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("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("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxCount") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatusLocked") + .HasColumnType("INTEGER"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYear") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYearLocked") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.HasIndex("Id", "SeriesId") + .IsUnique(); + + b.ToTable("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RelationKind") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("TargetSeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.HasIndex("TargetSeriesId"); + + b.ToTable("SeriesRelation"); + }); + + modelBuilder.Entity("API.Entities.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EndingMonth") + .HasColumnType("INTEGER"); + + b.Property("EndingYear") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("StartingMonth") + .HasColumnType("INTEGER"); + + b.Property("StartingYear") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("ReadingList"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ReadingListId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.HasIndex("ReadingListId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("VolumeId"); + + b.ToTable("ReadingListItem"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId1") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ScrobbleEventId1"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleError"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterNumber") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("IsProcessed") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("ProcessDateUtc") + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("ReviewBody") + .HasColumnType("TEXT"); + + b.Property("ReviewTitle") + .HasColumnType("TEXT"); + + b.Property("ScrobbleEventType") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeNumber") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("LibraryId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleEvent"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleHold"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderPath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastChapterAdded") + .HasColumnType("TEXT"); + + b.Property("LastChapterAddedUtc") + .HasColumnType("TEXT"); + + b.Property("LastFolderScanned") + .HasColumnType("TEXT"); + + b.Property("LastFolderScannedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("LocalizedNameLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedLocalizedName") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("SortNameLocked") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.ServerSetting", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("ServerSetting"); + }); + + modelBuilder.Entity("API.Entities.ServerStatistics", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterCount") + .HasColumnType("INTEGER"); + + b.Property("FileCount") + .HasColumnType("INTEGER"); + + b.Property("GenreCount") + .HasColumnType("INTEGER"); + + b.Property("PersonCount") + .HasColumnType("INTEGER"); + + b.Property("SeriesCount") + .HasColumnType("INTEGER"); + + b.Property("TagCount") + .HasColumnType("INTEGER"); + + b.Property("UserCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeCount") + .HasColumnType("INTEGER"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ServerStatistics"); + }); + + modelBuilder.Entity("API.Entities.SiteTheme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SiteTheme"); + }); + + modelBuilder.Entity("API.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("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("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "GenresId"); + + b.HasIndex("GenresId"); + + b.ToTable("ChapterGenre"); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.Property("ChapterMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterMetadatasId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.ToTable("ChapterPerson"); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("ChapterTag"); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.Property("CollectionTagsId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionTagsId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("CollectionTagSeriesMetadata"); + }); + + modelBuilder.Entity("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("PersonSeriesMetadata", b => + { + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("PeopleId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("PersonSeriesMetadata"); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("SeriesMetadatasId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("SeriesMetadataTag"); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("DashboardStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserExternalSource", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ExternalSources") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.SiteTheme", "Theme") + .WithMany() + .HasForeignKey("ThemeId"); + + b.Navigation("AppUser"); + + b.Navigation("Theme"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", null) + .WithMany("UserProgress") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Progress") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SideNavStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SmartFilters") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("TableOfContents") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserWantToRead", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("WantToRead") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Devices") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.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.ExternalRecommendation", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId"); + + b.Navigation("Series"); + }); + + 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.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("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChapterMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.HasOne("API.Entities.CollectionTag", null) + .WithMany() + .HasForeignKey("CollectionTagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("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("PersonSeriesMetadata", b => + { + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Bookmarks"); + + b.Navigation("DashboardStreams"); + + b.Navigation("Devices"); + + b.Navigation("ExternalSources"); + + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("ReadingLists"); + + b.Navigation("ScrobbleHolds"); + + b.Navigation("SideNavStreams"); + + b.Navigation("SmartFilters"); + + b.Navigation("TableOfContents"); + + b.Navigation("UserPreferences"); + + b.Navigation("UserRoles"); + + b.Navigation("WantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("Files"); + + 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.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/20240204141206_BlackListSeries.cs b/API/Data/Migrations/20240204141206_BlackListSeries.cs new file mode 100644 index 000000000..9e051e5a7 --- /dev/null +++ b/API/Data/Migrations/20240204141206_BlackListSeries.cs @@ -0,0 +1,57 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + /// + public partial class BlackListSeries : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "LastUpdatedUtc", + table: "ExternalSeriesMetadata", + newName: "ValidUntilUtc"); + + migrationBuilder.CreateTable( + name: "SeriesBlacklist", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + SeriesId = table.Column(type: "INTEGER", nullable: false), + LastChecked = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SeriesBlacklist", x => x.Id); + table.ForeignKey( + name: "FK_SeriesBlacklist_Series_SeriesId", + column: x => x.SeriesId, + principalTable: "Series", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_SeriesBlacklist_SeriesId", + table: "SeriesBlacklist", + column: "SeriesId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SeriesBlacklist"); + + migrationBuilder.RenameColumn( + name: "ValidUntilUtc", + table: "ExternalSeriesMetadata", + newName: "LastUpdatedUtc"); + } + } +} diff --git a/API/Data/Migrations/20240205184724_ScrobbleEventError.Designer.cs b/API/Data/Migrations/20240205184724_ScrobbleEventError.Designer.cs new file mode 100644 index 000000000..df5692eb4 --- /dev/null +++ b/API/Data/Migrations/20240205184724_ScrobbleEventError.Designer.cs @@ -0,0 +1,2880 @@ +// +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("20240205184724_ScrobbleEventError")] + partial class ScrobbleEventError + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("AgeRestriction") + .HasColumnType("INTEGER"); + + b.Property("AgeRestrictionIncludeUnknowns") + .HasColumnType("INTEGER"); + + b.Property("AniListAccessToken") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("ConfirmationToken") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LastActiveUtc") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Page") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserBookmark"); + }); + + modelBuilder.Entity("API.Entities.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("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("PromptForDownloadSize") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("ShareReviews") + .HasColumnType("INTEGER"); + + b.Property("ShowScreenHints") + .HasColumnType("INTEGER"); + + b.Property("SwipeToPaginate") + .HasColumnType("INTEGER"); + + b.Property("ThemeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.HasIndex("ThemeId"); + + b.ToTable("AppUserPreferences"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("HasBeenRated") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSourceId") + .HasColumnType("INTEGER"); + + b.Property("IsProvided") + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("SmartFilterId") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(5); + + b.Property("Visible") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SmartFilterId"); + + b.HasIndex("Visible"); + + b.ToTable("AppUserSideNavStream"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Filter") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserSmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PageNumber") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserTableOfContent"); + }); + + modelBuilder.Entity("API.Entities.AppUserWantToRead", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserWantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AlternateCount") + .HasColumnType("INTEGER"); + + b.Property("AlternateNumber") + .HasColumnType("TEXT"); + + b.Property("AlternateSeries") + .HasColumnType("TEXT"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("ISBN") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SeriesGroup") + .HasColumnType("TEXT"); + + b.Property("StoryArc") + .HasColumnType("TEXT"); + + b.Property("StoryArcNumber") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TitleName") + .HasColumnType("TEXT"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("API.Entities.CollectionTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Promoted") + .IsUnique(); + + b.ToTable("CollectionTag"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastUsed") + .HasColumnType("TEXT"); + + b.Property("LastUsedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Platform") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("Device"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowScrobbling") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderWatching") + .HasColumnType("INTEGER"); + + b.Property("IncludeInDashboard") + .HasColumnType("INTEGER"); + + b.Property("IncludeInRecommended") + .HasColumnType("INTEGER"); + + b.Property("IncludeInSearch") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("ManageCollections") + .HasColumnType("INTEGER"); + + b.Property("ManageReadingLists") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.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("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastFileAnalysis") + .HasColumnType("TEXT"); + + b.Property("LastFileAnalysisUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.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.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("AverageScore") + .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.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("Body") + .HasColumnType("TEXT"); + + b.Property("BodyJustText") + .HasColumnType("TEXT"); + + 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.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("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("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxCount") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatusLocked") + .HasColumnType("INTEGER"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYear") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYearLocked") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.HasIndex("Id", "SeriesId") + .IsUnique(); + + b.ToTable("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RelationKind") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("TargetSeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.HasIndex("TargetSeriesId"); + + b.ToTable("SeriesRelation"); + }); + + modelBuilder.Entity("API.Entities.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EndingMonth") + .HasColumnType("INTEGER"); + + b.Property("EndingYear") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("StartingMonth") + .HasColumnType("INTEGER"); + + b.Property("StartingYear") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("ReadingList"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ReadingListId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.HasIndex("ReadingListId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("VolumeId"); + + b.ToTable("ReadingListItem"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId1") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ScrobbleEventId1"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleError"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterNumber") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("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("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderPath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastChapterAdded") + .HasColumnType("TEXT"); + + b.Property("LastChapterAddedUtc") + .HasColumnType("TEXT"); + + b.Property("LastFolderScanned") + .HasColumnType("TEXT"); + + b.Property("LastFolderScannedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("LocalizedNameLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedLocalizedName") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("SortNameLocked") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.ServerSetting", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("ServerSetting"); + }); + + modelBuilder.Entity("API.Entities.ServerStatistics", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterCount") + .HasColumnType("INTEGER"); + + b.Property("FileCount") + .HasColumnType("INTEGER"); + + b.Property("GenreCount") + .HasColumnType("INTEGER"); + + b.Property("PersonCount") + .HasColumnType("INTEGER"); + + b.Property("SeriesCount") + .HasColumnType("INTEGER"); + + b.Property("TagCount") + .HasColumnType("INTEGER"); + + b.Property("UserCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeCount") + .HasColumnType("INTEGER"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ServerStatistics"); + }); + + modelBuilder.Entity("API.Entities.SiteTheme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SiteTheme"); + }); + + modelBuilder.Entity("API.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("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("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "GenresId"); + + b.HasIndex("GenresId"); + + b.ToTable("ChapterGenre"); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.Property("ChapterMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterMetadatasId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.ToTable("ChapterPerson"); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("ChapterTag"); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.Property("CollectionTagsId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionTagsId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("CollectionTagSeriesMetadata"); + }); + + modelBuilder.Entity("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("PersonSeriesMetadata", b => + { + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("PeopleId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("PersonSeriesMetadata"); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("SeriesMetadatasId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("SeriesMetadataTag"); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("DashboardStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserExternalSource", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ExternalSources") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.SiteTheme", "Theme") + .WithMany() + .HasForeignKey("ThemeId"); + + b.Navigation("AppUser"); + + b.Navigation("Theme"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", null) + .WithMany("UserProgress") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Progress") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SideNavStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SmartFilters") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("TableOfContents") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserWantToRead", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("WantToRead") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Devices") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.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.ExternalRecommendation", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId"); + + b.Navigation("Series"); + }); + + 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.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("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChapterMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.HasOne("API.Entities.CollectionTag", null) + .WithMany() + .HasForeignKey("CollectionTagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("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("PersonSeriesMetadata", b => + { + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Bookmarks"); + + b.Navigation("DashboardStreams"); + + b.Navigation("Devices"); + + b.Navigation("ExternalSources"); + + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("ReadingLists"); + + b.Navigation("ScrobbleHolds"); + + b.Navigation("SideNavStreams"); + + b.Navigation("SmartFilters"); + + b.Navigation("TableOfContents"); + + b.Navigation("UserPreferences"); + + b.Navigation("UserRoles"); + + b.Navigation("WantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("Files"); + + 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.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/20240205184724_ScrobbleEventError.cs b/API/Data/Migrations/20240205184724_ScrobbleEventError.cs new file mode 100644 index 000000000..5c8071b18 --- /dev/null +++ b/API/Data/Migrations/20240205184724_ScrobbleEventError.cs @@ -0,0 +1,57 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + /// + public partial class ScrobbleEventError : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "VolumeNumber", + table: "ScrobbleEvent", + type: "REAL", + nullable: true, + oldClrType: typeof(int), + oldType: "INTEGER", + oldNullable: true); + + migrationBuilder.AddColumn( + name: "ErrorDetails", + table: "ScrobbleEvent", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "IsErrored", + table: "ScrobbleEvent", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ErrorDetails", + table: "ScrobbleEvent"); + + migrationBuilder.DropColumn( + name: "IsErrored", + table: "ScrobbleEvent"); + + migrationBuilder.AlterColumn( + name: "VolumeNumber", + table: "ScrobbleEvent", + type: "INTEGER", + nullable: true, + oldClrType: typeof(float), + oldType: "REAL", + oldNullable: true); + } + } +} diff --git a/API/Data/Migrations/20240209224347_DBTweaks.Designer.cs b/API/Data/Migrations/20240209224347_DBTweaks.Designer.cs new file mode 100644 index 000000000..0afb2e5cb --- /dev/null +++ b/API/Data/Migrations/20240209224347_DBTweaks.Designer.cs @@ -0,0 +1,2871 @@ +// +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("20240209224347_DBTweaks")] + partial class DBTweaks + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("AgeRestriction") + .HasColumnType("INTEGER"); + + b.Property("AgeRestrictionIncludeUnknowns") + .HasColumnType("INTEGER"); + + b.Property("AniListAccessToken") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("ConfirmationToken") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LastActiveUtc") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Page") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserBookmark"); + }); + + modelBuilder.Entity("API.Entities.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("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("PromptForDownloadSize") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("ShareReviews") + .HasColumnType("INTEGER"); + + b.Property("ShowScreenHints") + .HasColumnType("INTEGER"); + + b.Property("SwipeToPaginate") + .HasColumnType("INTEGER"); + + b.Property("ThemeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.HasIndex("ThemeId"); + + b.ToTable("AppUserPreferences"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("HasBeenRated") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSourceId") + .HasColumnType("INTEGER"); + + b.Property("IsProvided") + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("SmartFilterId") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(5); + + b.Property("Visible") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SmartFilterId"); + + b.HasIndex("Visible"); + + b.ToTable("AppUserSideNavStream"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Filter") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserSmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PageNumber") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserTableOfContent"); + }); + + modelBuilder.Entity("API.Entities.AppUserWantToRead", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserWantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AlternateCount") + .HasColumnType("INTEGER"); + + b.Property("AlternateNumber") + .HasColumnType("TEXT"); + + b.Property("AlternateSeries") + .HasColumnType("TEXT"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("ISBN") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SeriesGroup") + .HasColumnType("TEXT"); + + b.Property("StoryArc") + .HasColumnType("TEXT"); + + b.Property("StoryArcNumber") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TitleName") + .HasColumnType("TEXT"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("API.Entities.CollectionTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Promoted") + .IsUnique(); + + b.ToTable("CollectionTag"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastUsed") + .HasColumnType("TEXT"); + + b.Property("LastUsedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Platform") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("Device"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowScrobbling") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderWatching") + .HasColumnType("INTEGER"); + + b.Property("IncludeInDashboard") + .HasColumnType("INTEGER"); + + b.Property("IncludeInRecommended") + .HasColumnType("INTEGER"); + + b.Property("IncludeInSearch") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("ManageCollections") + .HasColumnType("INTEGER"); + + b.Property("ManageReadingLists") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.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("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastFileAnalysis") + .HasColumnType("TEXT"); + + b.Property("LastFileAnalysisUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.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.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("AverageScore") + .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.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("Body") + .HasColumnType("TEXT"); + + b.Property("BodyJustText") + .HasColumnType("TEXT"); + + 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.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("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("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxCount") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatusLocked") + .HasColumnType("INTEGER"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYear") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYearLocked") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.HasIndex("Id", "SeriesId") + .IsUnique(); + + b.ToTable("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RelationKind") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("TargetSeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.HasIndex("TargetSeriesId"); + + b.ToTable("SeriesRelation"); + }); + + modelBuilder.Entity("API.Entities.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EndingMonth") + .HasColumnType("INTEGER"); + + b.Property("EndingYear") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("StartingMonth") + .HasColumnType("INTEGER"); + + b.Property("StartingYear") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("ReadingList"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ReadingListId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.HasIndex("ReadingListId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("VolumeId"); + + b.ToTable("ReadingListItem"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId1") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ScrobbleEventId1"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleError"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterNumber") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("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("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderPath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastChapterAdded") + .HasColumnType("TEXT"); + + b.Property("LastChapterAddedUtc") + .HasColumnType("TEXT"); + + b.Property("LastFolderScanned") + .HasColumnType("TEXT"); + + b.Property("LastFolderScannedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("LocalizedNameLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedLocalizedName") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("SortNameLocked") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.ServerSetting", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("ServerSetting"); + }); + + modelBuilder.Entity("API.Entities.ServerStatistics", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterCount") + .HasColumnType("INTEGER"); + + b.Property("FileCount") + .HasColumnType("INTEGER"); + + b.Property("GenreCount") + .HasColumnType("INTEGER"); + + b.Property("PersonCount") + .HasColumnType("INTEGER"); + + b.Property("SeriesCount") + .HasColumnType("INTEGER"); + + b.Property("TagCount") + .HasColumnType("INTEGER"); + + b.Property("UserCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeCount") + .HasColumnType("INTEGER"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ServerStatistics"); + }); + + modelBuilder.Entity("API.Entities.SiteTheme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SiteTheme"); + }); + + modelBuilder.Entity("API.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("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("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "GenresId"); + + b.HasIndex("GenresId"); + + b.ToTable("ChapterGenre"); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.Property("ChapterMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterMetadatasId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.ToTable("ChapterPerson"); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("ChapterTag"); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.Property("CollectionTagsId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionTagsId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("CollectionTagSeriesMetadata"); + }); + + modelBuilder.Entity("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("PersonSeriesMetadata", b => + { + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("PeopleId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("PersonSeriesMetadata"); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("SeriesMetadatasId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("SeriesMetadataTag"); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("DashboardStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserExternalSource", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ExternalSources") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.SiteTheme", "Theme") + .WithMany() + .HasForeignKey("ThemeId"); + + b.Navigation("AppUser"); + + b.Navigation("Theme"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", null) + .WithMany("UserProgress") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Progress") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SideNavStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SmartFilters") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("TableOfContents") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserWantToRead", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("WantToRead") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Devices") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.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.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.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("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChapterMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.HasOne("API.Entities.CollectionTag", null) + .WithMany() + .HasForeignKey("CollectionTagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("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("PersonSeriesMetadata", b => + { + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Bookmarks"); + + b.Navigation("DashboardStreams"); + + b.Navigation("Devices"); + + b.Navigation("ExternalSources"); + + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("ReadingLists"); + + b.Navigation("ScrobbleHolds"); + + b.Navigation("SideNavStreams"); + + b.Navigation("SmartFilters"); + + b.Navigation("TableOfContents"); + + b.Navigation("UserPreferences"); + + b.Navigation("UserRoles"); + + b.Navigation("WantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("Files"); + + 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.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/20240209224347_DBTweaks.cs b/API/Data/Migrations/20240209224347_DBTweaks.cs new file mode 100644 index 000000000..797905930 --- /dev/null +++ b/API/Data/Migrations/20240209224347_DBTweaks.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + /// + public partial class DBTweaks : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_ExternalRecommendation_Series_SeriesId", + table: "ExternalRecommendation"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddForeignKey( + name: "FK_ExternalRecommendation_Series_SeriesId", + table: "ExternalRecommendation", + column: "SeriesId", + principalTable: "Series", + principalColumn: "Id"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 4299492ac..d2f0f6240 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -15,7 +15,7 @@ namespace API.Data.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "7.0.13"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); modelBuilder.Entity("API.Entities.AppRole", b => { @@ -602,6 +602,27 @@ namespace API.Data.Migrations 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") @@ -980,6 +1001,26 @@ namespace API.Data.Migrations b.ToTable("MangaFile"); }); + modelBuilder.Entity("API.Entities.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.MediaError", b => { b.Property("Id") @@ -1015,6 +1056,164 @@ namespace API.Data.Migrations b.ToTable("MediaError"); }); + modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AverageScore") + .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.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("Body") + .HasColumnType("TEXT"); + + b.Property("BodyJustText") + .HasColumnType("TEXT"); + + 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.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("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") @@ -1323,9 +1522,15 @@ namespace API.Data.Migrations 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"); @@ -1359,8 +1564,8 @@ namespace API.Data.Migrations b.Property("SeriesId") .HasColumnType("INTEGER"); - b.Property("VolumeNumber") - .HasColumnType("INTEGER"); + b.Property("VolumeNumber") + .HasColumnType("REAL"); b.HasKey("Id"); @@ -1412,9 +1617,6 @@ namespace API.Data.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("AppUserId") - .HasColumnType("INTEGER"); - b.Property("AvgHoursToRead") .HasColumnType("INTEGER"); @@ -1495,8 +1697,6 @@ namespace API.Data.Migrations b.HasKey("Id"); - b.HasIndex("AppUserId"); - b.HasIndex("LibraryId"); b.ToTable("Series"); @@ -1642,9 +1842,15 @@ namespace API.Data.Migrations 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"); @@ -1742,6 +1948,51 @@ namespace API.Data.Migrations 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") @@ -2062,6 +2313,25 @@ namespace API.Data.Migrations 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") @@ -2128,6 +2398,28 @@ namespace API.Data.Migrations b.Navigation("Chapter"); }); + 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") @@ -2269,10 +2561,6 @@ namespace API.Data.Migrations modelBuilder.Entity("API.Entities.Series", b => { - b.HasOne("API.Entities.AppUser", null) - .WithMany("WantToRead") - .HasForeignKey("AppUserId"); - b.HasOne("API.Entities.Library", "Library") .WithMany("Series") .HasForeignKey("LibraryId") @@ -2368,6 +2656,51 @@ namespace API.Data.Migrations .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) @@ -2510,6 +2843,8 @@ namespace API.Data.Migrations modelBuilder.Entity("API.Entities.Series", b => { + b.Navigation("ExternalSeriesMetadata"); + b.Navigation("Metadata"); b.Navigation("Progress"); diff --git a/API/Data/Misc/RecentlyAddedSeries.cs b/API/Data/Misc/RecentlyAddedSeries.cs index d5cfece45..1ea5b1d3e 100644 --- a/API/Data/Misc/RecentlyAddedSeries.cs +++ b/API/Data/Misc/RecentlyAddedSeries.cs @@ -18,6 +18,6 @@ public class RecentlyAddedSeries public string? ChapterRange { get; init; } public string? ChapterTitle { get; init; } public bool IsSpecial { get; init; } - public int VolumeNumber { get; init; } + public float VolumeNumber { get; init; } public AgeRating AgeRating { get; init; } } diff --git a/API/Data/Repositories/AppUserProgressRepository.cs b/API/Data/Repositories/AppUserProgressRepository.cs index 2d1033621..2878938a5 100644 --- a/API/Data/Repositories/AppUserProgressRepository.cs +++ b/API/Data/Repositories/AppUserProgressRepository.cs @@ -32,7 +32,7 @@ public interface IAppUserProgressRepository Task GetUserProgressDtoAsync(int chapterId, int userId); Task AnyUserProgressForSeriesAsync(int seriesId, int userId); Task GetHighestFullyReadChapterForSeries(int seriesId, int userId); - Task GetHighestFullyReadVolumeForSeries(int seriesId, int userId); + Task GetHighestFullyReadVolumeForSeries(int seriesId, int userId); Task GetLatestProgressForSeries(int seriesId, int userId); Task GetFirstProgressForSeries(int seriesId, int userId); Task UpdateAllProgressThatAreMoreThanChapterPages(); @@ -172,14 +172,14 @@ public class AppUserProgressRepository : IAppUserProgressRepository return list.Count == 0 ? 0 : list.DefaultIfEmpty().Where(d => d != null).Max(d => (int) Math.Floor(Parser.MaxNumberFromRange(d))); } - public async Task GetHighestFullyReadVolumeForSeries(int seriesId, int userId) + public async Task GetHighestFullyReadVolumeForSeries(int seriesId, int userId) { var list = await _context.AppUserProgresses .Join(_context.Chapter, appUserProgresses => appUserProgresses.ChapterId, chapter => chapter.Id, (appUserProgresses, chapter) => new {appUserProgresses, chapter}) .Where(p => p.appUserProgresses.SeriesId == seriesId && p.appUserProgresses.AppUserId == userId && p.appUserProgresses.PagesRead >= p.chapter.Pages) - .Select(p => p.chapter.Volume.Number) + .Select(p => p.chapter.Volume.MaxNumber) .ToListAsync(); return list.Count == 0 ? 0 : list.DefaultIfEmpty().Max(); } diff --git a/API/Data/Repositories/AppUserSmartFilterRepository.cs b/API/Data/Repositories/AppUserSmartFilterRepository.cs index fd8933634..c7f981daa 100644 --- a/API/Data/Repositories/AppUserSmartFilterRepository.cs +++ b/API/Data/Repositories/AppUserSmartFilterRepository.cs @@ -8,6 +8,7 @@ using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; +#nullable enable public interface IAppUserSmartFilterRepository { @@ -55,6 +56,7 @@ public class AppUserSmartFilterRepository : IAppUserSmartFilterRepository public async Task GetById(int smartFilterId) { - return await _context.AppUserSmartFilter.FirstOrDefaultAsync(d => d.Id == smartFilterId); + return await _context.AppUserSmartFilter + .FirstOrDefaultAsync(d => d.Id == smartFilterId); } } diff --git a/API/Data/Repositories/ChapterRepository.cs b/API/Data/Repositories/ChapterRepository.cs index 106de2386..a9fbf3ce3 100644 --- a/API/Data/Repositories/ChapterRepository.cs +++ b/API/Data/Repositories/ChapterRepository.cs @@ -14,6 +14,7 @@ using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; +#nullable enable [Flags] public enum ChapterIncludes diff --git a/API/Data/Repositories/ExternalSeriesMetadataRepository.cs b/API/Data/Repositories/ExternalSeriesMetadataRepository.cs new file mode 100644 index 000000000..31de47d21 --- /dev/null +++ b/API/Data/Repositories/ExternalSeriesMetadataRepository.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using API.Constants; +using API.DTOs; +using API.DTOs.Recommendation; +using API.DTOs.Scrobbling; +using API.DTOs.SeriesDetail; +using API.Entities; +using API.Entities.Enums; +using API.Entities.Metadata; +using API.Extensions; +using API.Extensions.QueryExtensions; +using API.Services.Plus; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; + +namespace API.Data.Repositories; +#nullable enable + +public interface IExternalSeriesMetadataRepository +{ + void Attach(ExternalSeriesMetadata metadata); + void Attach(ExternalRating rating); + void Attach(ExternalReview review); + void Remove(IEnumerable? reviews); + void Remove(IEnumerable? ratings); + void Remove(IEnumerable? recommendations); + void Remove(ExternalSeriesMetadata metadata); + Task GetExternalSeriesMetadata(int seriesId); + Task ExternalSeriesMetadataNeedsRefresh(int seriesId); + Task GetSeriesDetailPlusDto(int seriesId); + Task LinkRecommendationsToSeries(Series series); + Task IsBlacklistedSeries(int seriesId); + Task CreateBlacklistedSeries(int seriesId, bool saveChanges = true); + Task RemoveFromBlacklist(int seriesId); + Task> GetAllSeriesIdsWithoutMetadata(int limit); +} + +public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepository +{ + private readonly DataContext _context; + private readonly IMapper _mapper; + + public ExternalSeriesMetadataRepository(DataContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public void Attach(ExternalSeriesMetadata metadata) + { + _context.ExternalSeriesMetadata.Attach(metadata); + } + + public void Attach(ExternalRating rating) + { + _context.ExternalRating.Attach(rating); + } + + public void Attach(ExternalReview review) + { + _context.ExternalReview.Attach(review); + } + + public void Remove(IEnumerable? reviews) + { + if (reviews == null) return; + _context.ExternalReview.RemoveRange(reviews); + } + + public void Remove(IEnumerable? ratings) + { + if (ratings == null) return; + _context.ExternalRating.RemoveRange(ratings); + } + + public void Remove(IEnumerable? recommendations) + { + if (recommendations == null) return; + _context.ExternalRecommendation.RemoveRange(recommendations); + } + + public void Remove(ExternalSeriesMetadata? metadata) + { + if (metadata == null) return; + _context.ExternalSeriesMetadata.Remove(metadata); + } + + /// + /// Returns the ExternalSeriesMetadata entity for the given Series including all linked tables + /// + /// + /// + public Task GetExternalSeriesMetadata(int seriesId) + { + return _context.ExternalSeriesMetadata + .Where(s => s.SeriesId == seriesId) + .Include(s => s.ExternalReviews) + .Include(s => s.ExternalRatings.OrderBy(r => r.AverageScore)) + .Include(s => s.ExternalRecommendations.OrderBy(r => r.Id)) + .AsSplitQuery() + .FirstOrDefaultAsync(); + } + + public async Task ExternalSeriesMetadataNeedsRefresh(int seriesId) + { + var row = await _context.ExternalSeriesMetadata + .Where(s => s.SeriesId == seriesId) + .FirstOrDefaultAsync(); + return row == null || row.ValidUntilUtc <= DateTime.UtcNow; + } + + public async Task GetSeriesDetailPlusDto(int seriesId) + { + var seriesDetailDto = await _context.ExternalSeriesMetadata + .Where(m => m.SeriesId == seriesId) + .Include(m => m.ExternalRatings) + .Include(m => m.ExternalReviews) + .Include(m => m.ExternalRecommendations) + .FirstOrDefaultAsync(); + + if (seriesDetailDto == null) + { + return null; // or handle the case when seriesDetailDto is not found + } + + var externalSeriesRecommendations = seriesDetailDto.ExternalRecommendations + .Where(r => r.SeriesId == null) + .Select(r => _mapper.Map(r)) + .ToList(); + + var ownedIds = seriesDetailDto.ExternalRecommendations + .Where(r => r.SeriesId != null) + .Select(r => r.SeriesId) + .ToList(); + + var ownedSeriesRecommendations = await _context.Series + .Where(s => ownedIds.Contains(s.Id)) + .OrderBy(s => s.SortName.ToLower()) + .ProjectTo(_mapper.ConfigurationProvider) + .ToListAsync(); + + IEnumerable reviews = new List(); + if (seriesDetailDto.ExternalReviews != null && seriesDetailDto.ExternalReviews.Any()) + { + reviews = seriesDetailDto.ExternalReviews + .Select(r => + { + var ret = _mapper.Map(r); + ret.IsExternal = true; + return ret; + }) + .OrderByDescending(r => r.Score); + } + + IEnumerable ratings = new List(); + if (seriesDetailDto.ExternalRatings != null && seriesDetailDto.ExternalRatings.Any()) + { + ratings = seriesDetailDto.ExternalRatings + .Select(r => _mapper.Map(r)); + } + + + var seriesDetailPlusDto = new SeriesDetailPlusDto() + { + Ratings = ratings, + Reviews = reviews, + Recommendations = new RecommendationDto() + { + ExternalSeries = externalSeriesRecommendations, + OwnedSeries = ownedSeriesRecommendations + } + }; + + return seriesDetailPlusDto; + } + + /// + /// Searches Recommendations without a SeriesId on record and attempts to link based on Series Name/Localized Name + /// + /// + /// + public async Task LinkRecommendationsToSeries(Series series) + { + var recMatches = await _context.ExternalRecommendation + .Where(r => r.SeriesId == null || r.SeriesId == 0) + .Where(r => EF.Functions.Like(r.Name, series.Name) || + EF.Functions.Like(r.Name, series.LocalizedName)) + .ToListAsync(); + foreach (var rec in recMatches) + { + rec.SeriesId = series.Id; + } + + await _context.SaveChangesAsync(); + } + + public Task IsBlacklistedSeries(int seriesId) + { + return _context.SeriesBlacklist.AnyAsync(s => s.SeriesId == seriesId); + } + + /// + /// Creates a new instance against SeriesId and Saves to the DB + /// + /// + /// + public async Task CreateBlacklistedSeries(int seriesId, bool saveChanges = true) + { + if (seriesId <= 0 || await _context.SeriesBlacklist.AnyAsync(s => s.SeriesId == seriesId)) return; + + await _context.SeriesBlacklist.AddAsync(new SeriesBlacklist() + { + SeriesId = seriesId + }); + if (saveChanges) + { + await _context.SaveChangesAsync(); + } + } + + /// + /// Removes the Series from Blacklist and Saves to the DB + /// + /// + public async Task RemoveFromBlacklist(int seriesId) + { + var seriesBlacklist = await _context.SeriesBlacklist.FirstOrDefaultAsync(sb => sb.SeriesId == seriesId); + + if (seriesBlacklist != null) + { + // Remove the SeriesBlacklist entity from the context + _context.SeriesBlacklist.Remove(seriesBlacklist); + + // Save the changes to the database + await _context.SaveChangesAsync(); + } + } + + public async Task> GetAllSeriesIdsWithoutMetadata(int limit) + { + return await _context.Series + .Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type)) + .Where(s => s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.ValidUntilUtc < DateTime.UtcNow) + .OrderByDescending(s => s.Library.Type) + .ThenBy(s => s.NormalizedName) + .Select(s => s.Id) + .Take(limit) + .ToListAsync(); + } +} diff --git a/API/Data/Repositories/LibraryRepository.cs b/API/Data/Repositories/LibraryRepository.cs index 7c61bc890..d6d562b82 100644 --- a/API/Data/Repositories/LibraryRepository.cs +++ b/API/Data/Repositories/LibraryRepository.cs @@ -43,6 +43,7 @@ public interface ILibraryRepository Task> GetLibrariesForUserIdAsync(int userId); IEnumerable GetLibraryIdsForUserIdAsync(int userId, QueryContext queryContext = QueryContext.None); Task GetLibraryTypeAsync(int libraryId); + Task GetLibraryTypeBySeriesIdAsync(int seriesId); Task> GetLibraryForIdsAsync(IEnumerable libraryIds, LibraryIncludes includes = LibraryIncludes.None); Task GetTotalFiles(); IEnumerable GetJumpBarAsync(int libraryId); @@ -54,6 +55,8 @@ public interface ILibraryRepository Task> GetAllCoverImagesAsync(); Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat); Task GetAllowsScrobblingBySeriesId(int seriesId); + + Task> GetLibraryTypesBySeriesIdsAsync(IList seriesIds); } public class LibraryRepository : ILibraryRepository @@ -106,6 +109,7 @@ public class LibraryRepository : ILibraryRepository return await _context.Library .Include(l => l.AppUsers) .Includes(includes) + .AsSplitQuery() .ToListAsync(); } @@ -141,6 +145,14 @@ public class LibraryRepository : ILibraryRepository .FirstAsync(); } + public async Task GetLibraryTypeBySeriesIdAsync(int seriesId) + { + return await _context.Series + .Where(s => s.Id == seriesId) + .Select(s => s.Library.Type) + .FirstAsync(); + } + public async Task> GetLibraryForIdsAsync(IEnumerable libraryIds, LibraryIncludes includes = LibraryIncludes.None) { return await _context.Library @@ -341,4 +353,16 @@ public class LibraryRepository : ILibraryRepository .Select(s => s.Library.AllowScrobbling) .SingleOrDefaultAsync(); } + + public async Task> GetLibraryTypesBySeriesIdsAsync(IList seriesIds) + { + return await _context.Series + .Where(series => seriesIds.Contains(series.Id)) + .Select(series => new + { + series.Id, + series.Library.Type + }) + .ToDictionaryAsync(entity => entity.Id, entity => entity.Type); + } } diff --git a/API/Data/Repositories/ScrobbleEventRepository.cs b/API/Data/Repositories/ScrobbleEventRepository.cs index bff82c681..7d3567831 100644 --- a/API/Data/Repositories/ScrobbleEventRepository.cs +++ b/API/Data/Repositories/ScrobbleEventRepository.cs @@ -27,7 +27,7 @@ public interface IScrobbleRepository Task ClearScrobbleErrors(); Task HasErrorForSeries(int seriesId); Task GetEvent(int userId, int seriesId, ScrobbleEventType eventType); - Task> GetUserEvents(int userId); + Task> GetUserEventsForSeries(int userId, int seriesId); Task> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination); } @@ -127,16 +127,17 @@ public class ScrobbleRepository : IScrobbleRepository return await _context.ScrobbleEvent.FirstOrDefaultAsync(e => e.AppUserId == userId && e.SeriesId == seriesId && e.ScrobbleEventType == eventType); } - public async Task> GetUserEvents(int userId) + + public async Task> GetUserEventsForSeries(int userId, int seriesId) { return await _context.ScrobbleEvent - .Where(e => e.AppUserId == userId) + .Where(e => e.AppUserId == userId && !e.IsProcessed) .Include(e => e.Series) .OrderBy(e => e.LastModifiedUtc) .AsSplitQuery() - .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } + public async Task> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination) { var query = _context.ScrobbleEvent @@ -146,6 +147,7 @@ public class ScrobbleRepository : IScrobbleRepository .WhereIf(!string.IsNullOrEmpty(filter.Query), s => EF.Functions.Like(s.Series.Name, $"%{filter.Query}%") ) + .WhereIf(!filter.IncludeReviews, e => e.ScrobbleEventType != ScrobbleEventType.Review) .AsSplitQuery() .ProjectTo(_mapper.ConfigurationProvider); diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index 5d2dc30b7..9c6dcc2bb 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; -using API.Data.ManualMigrations; +using API.Constants; using API.Data.Misc; using API.Data.Scanner; using API.DTOs; @@ -13,8 +12,8 @@ using API.DTOs.Dashboard; using API.DTOs.Filtering; using API.DTOs.Filtering.v2; using API.DTOs.Metadata; -using API.DTOs.Reader; using API.DTOs.ReadingLists; +using API.DTOs.Scrobbling; using API.DTOs.Search; using API.DTOs.SeriesDetail; using API.DTOs.Settings; @@ -27,12 +26,13 @@ using API.Extensions.QueryExtensions.Filtering; using API.Helpers; using API.Helpers.Converters; using API.Services; +using API.Services.Plus; using API.Services.Tasks; using API.Services.Tasks.Scanner; using AutoMapper; using AutoMapper.QueryableExtensions; +using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; -using SQLite; namespace API.Data.Repositories; @@ -45,7 +45,12 @@ public enum SeriesIncludes Metadata = 4, Related = 8, Library = 16, - Chapters = 32 + Chapters = 32, + ExternalReviews = 64, + ExternalRatings = 128, + ExternalRecommendations = 256, + ExternalMetadata = 512 + } /// @@ -89,6 +94,7 @@ public interface ISeriesRepository Task> GetSeriesForLibraryIdAsync(int libraryId, SeriesIncludes includes = SeriesIncludes.None); Task GetSeriesDtoByIdAsync(int seriesId, int userId); Task GetSeriesByIdAsync(int seriesId, SeriesIncludes includes = SeriesIncludes.Volumes | SeriesIncludes.Metadata); + Task> GetSeriesDtoByIdsAsync(IEnumerable seriesIds, AppUser user); Task> GetSeriesByIdsAsync(IList seriesIds); Task GetChapterIdsForSeriesAsync(IList seriesIds); Task>> GetChapterIdWithSeriesIdForSeriesAsync(int[] seriesIds); @@ -131,6 +137,8 @@ public interface ISeriesRepository Task> GetAllSeriesByNameAsync(IList normalizedNames, int userId, SeriesIncludes includes = SeriesIncludes.None); Task GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, MangaFormat format, bool withFullIncludes = true); + public Task> GetAllSeriesByAnyName(string seriesName, string localizedName, int libraryId, + MangaFormat format); Task> RemoveSeriesNotInList(IList seenSeries, int libraryId); Task>> GetFolderPathMap(int libraryId); Task GetMaxAgeRatingFromSeriesAsync(IEnumerable seriesIds); @@ -141,25 +149,28 @@ public interface ISeriesRepository Task> GetLibraryIdsForSeriesAsync(); Task> GetSeriesMetadataForIds(IEnumerable seriesIds); Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, bool customOnly = true); - Task GetSeriesDtoByNamesAndMetadataIdsForUser(int userId, IEnumerable names, LibraryType libraryType, string aniListUrl, string malUrl); + Task GetSeriesDtoByNamesAndMetadataIds(IEnumerable names, LibraryType libraryType, string aniListUrl, string malUrl); Task GetAverageUserRating(int seriesId, int userId); Task RemoveFromOnDeck(int seriesId, int userId); Task ClearOnDeckRemoval(int seriesId, int userId); Task> GetSeriesDtoForLibraryIdV2Async(int userId, UserParams userParams, FilterV2Dto filterDto); + Task GetPlusSeriesDto(int seriesId); } public class SeriesRepository : ISeriesRepository { private readonly DataContext _context; private readonly IMapper _mapper; + private readonly UserManager _userManager; private readonly Regex _yearRegex = new Regex(@"\d{4}", RegexOptions.Compiled, Services.Tasks.Scanner.Parser.Parser.RegexTimeout); - public SeriesRepository(DataContext context, IMapper mapper) + public SeriesRepository(DataContext context, IMapper mapper, UserManager userManager) { _context = context; _mapper = mapper; + _userManager = userManager; } public void Add(Series series) @@ -172,6 +183,11 @@ public class SeriesRepository : ISeriesRepository _context.Series.Attach(series); } + public void Attach(ExternalSeriesMetadata metadata) + { + _context.ExternalSeriesMetadata.Attach(metadata); + } + public void Update(Series series) { _context.Entry(series).State = EntityState.Modified; @@ -350,8 +366,8 @@ public class SeriesRepository : ISeriesRepository .Where(l => EF.Functions.Like(l.Name, $"%{searchQuery}%")) .IsRestricted(QueryContext.Search) .AsSplitQuery() - .Take(maxRecords) .OrderBy(l => l.Name.ToLower()) + .Take(maxRecords) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); @@ -370,8 +386,8 @@ public class SeriesRepository : ISeriesRepository .Include(s => s.Library) .AsNoTracking() .AsSplitQuery() - .Take(maxRecords) .OrderBy(s => s.SortName!.ToLower()) + .Take(maxRecords) .ProjectTo(_mapper.ConfigurationProvider) .AsEnumerable(); @@ -407,8 +423,8 @@ public class SeriesRepository : ISeriesRepository .Where(rl => EF.Functions.Like(rl.Title, $"%{searchQuery}%")) .RestrictAgainstAgeRestriction(userRating) .AsSplitQuery() - .Take(maxRecords) .OrderBy(r => r.NormalizedTitle) + .Take(maxRecords) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); @@ -418,7 +434,6 @@ public class SeriesRepository : ISeriesRepository .Where(c => c.Promoted || isAdmin) .RestrictAgainstAgeRestriction(userRating) .OrderBy(s => s.NormalizedTitle) - .AsNoTracking() .AsSplitQuery() .Take(maxRecords) .OrderBy(c => c.NormalizedTitle) @@ -430,8 +445,8 @@ public class SeriesRepository : ISeriesRepository .SelectMany(sm => sm.People.Where(t => t.Name != null && EF.Functions.Like(t.Name, $"%{searchQuery}%"))) .AsSplitQuery() .Distinct() - .Take(maxRecords) .OrderBy(p => p.NormalizedName) + .Take(maxRecords) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); @@ -440,8 +455,8 @@ public class SeriesRepository : ISeriesRepository .SelectMany(sm => sm.Genres.Where(t => EF.Functions.Like(t.Title, $"%{searchQuery}%"))) .AsSplitQuery() .Distinct() - .Take(maxRecords) .OrderBy(t => t.NormalizedTitle) + .Take(maxRecords) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); @@ -450,8 +465,8 @@ public class SeriesRepository : ISeriesRepository .SelectMany(sm => sm.Tags.Where(t => EF.Functions.Like(t.Title, $"%{searchQuery}%"))) .AsSplitQuery() .Distinct() - .Take(maxRecords) .OrderBy(t => t.NormalizedTitle) + .Take(maxRecords) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); @@ -462,14 +477,22 @@ public class SeriesRepository : ISeriesRepository .SelectMany(v => v.Chapters) .SelectMany(c => c.Files.Select(f => f.Id)); - result.Files = await _context.MangaFile - .Where(m => EF.Functions.Like(m.FilePath, $"%{searchQuery}%") && fileIds.Contains(m.Id)) - .AsSplitQuery() - .Take(maxRecords) - .OrderBy(f => f.FilePath) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - + // Need to check if an admin + var user = await _context.AppUser.FirstAsync(u => u.Id == userId); + if (await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole)) + { + result.Files = await _context.MangaFile + .Where(m => EF.Functions.Like(m.FilePath, $"%{searchQuery}%") && fileIds.Contains(m.Id)) + .AsSplitQuery() + .OrderBy(f => f.FilePath) + .Take(maxRecords) + .ProjectTo(_mapper.ConfigurationProvider) + .ToListAsync(); + } + else + { + result.Files = new List(); + } result.Chapters = await _context.Chapter .Include(c => c.Files) @@ -478,8 +501,8 @@ public class SeriesRepository : ISeriesRepository ) .Where(c => c.Files.All(f => fileIds.Contains(f.Id))) .AsSplitQuery() - .Take(maxRecords) .OrderBy(c => c.TitleName) + .Take(maxRecords) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); @@ -529,7 +552,7 @@ public class SeriesRepository : ISeriesRepository } /// - /// Returns Volumes, Metadata, and Collection Tags + /// Returns Full Series including all external links /// /// /// @@ -537,14 +560,45 @@ public class SeriesRepository : ISeriesRepository { return await _context.Series .Include(s => s.Volumes) + .Include(s => s.Relations) .Include(s => s.Metadata) .ThenInclude(m => m.CollectionTags) - .Include(s => s.Relations) + + + .Include(s => s.ExternalSeriesMetadata) + + .Include(s => s.ExternalSeriesMetadata) + .ThenInclude(e => e.ExternalRatings) + .Include(s => s.ExternalSeriesMetadata) + .ThenInclude(e => e.ExternalReviews) + .Include(s => s.ExternalSeriesMetadata) + .ThenInclude(e => e.ExternalRecommendations) + .Where(s => seriesIds.Contains(s.Id)) .AsSplitQuery() .ToListAsync(); } + public async Task> GetSeriesDtoByIdsAsync(IEnumerable seriesIds, AppUser user) + { + var allowedLibraries = await _context.Library + .Where(library => library.AppUsers.Any(x => x.Id == user.Id)) + .Select(l => l.Id) + .ToListAsync(); + var restriction = new AgeRestriction() + { + AgeRating = user.AgeRestriction, + IncludeUnknowns = user.AgeRestrictionIncludeUnknowns + }; + return await _context.Series + .Include(s => s.Metadata) + .Where(s => seriesIds.Contains(s.Id) && allowedLibraries.Contains(s.LibraryId)) + .RestrictAgainstAgeRestriction(restriction) + .AsSplitQuery() + .ProjectTo(_mapper.ConfigurationProvider) + .ToListAsync(); + } + public async Task GetChapterIdsForSeriesAsync(IList seriesIds) { var volumes = await _context.Volume @@ -661,6 +715,29 @@ public class SeriesRepository : ISeriesRepository return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize); } + public async Task GetPlusSeriesDto(int seriesId) + { + return await _context.Series + .Where(s => s.Id == seriesId) + .Select(series => new PlusSeriesDto() + { + MediaFormat = LibraryTypeHelper.GetFormat(series.Library.Type), + SeriesName = series.Name, + AltSeriesName = series.LocalizedName, + AniListId = ScrobblingService.ExtractId(series.Metadata.WebLinks, + ScrobblingService.AniListWeblinkWebsite), + MalId = ScrobblingService.ExtractId(series.Metadata.WebLinks, + ScrobblingService.MalWeblinkWebsite), + GoogleBooksId = ScrobblingService.ExtractId(series.Metadata.WebLinks, + ScrobblingService.GoogleBooksWeblinkWebsite), + MangaDexId = ScrobblingService.ExtractId(series.Metadata.WebLinks, + ScrobblingService.MangaDexWeblinkWebsite), + VolumeCount = series.Volumes.Count, + ChapterCount = series.Volumes.SelectMany(v => v.Chapters).Count(c => !c.IsSpecial), + Year = series.Metadata.ReleaseYear + }) + .FirstOrDefaultAsync(); + } public async Task AddSeriesModifiers(int userId, IList series) { @@ -951,6 +1028,8 @@ public class SeriesRepository : ISeriesRepository SortField.TimeToRead => query.DoOrderBy(s => s.AvgHoursToRead, filter.SortOptions), SortField.ReleaseYear => query.DoOrderBy(s => s.Metadata.ReleaseYear, filter.SortOptions), SortField.ReadProgress => query.DoOrderBy(s => s.Progress.Where(p => p.SeriesId == s.Id).Select(p => p.LastModified).Max(), filter.SortOptions), + SortField.AverageRating => query.DoOrderBy(s => s.ExternalSeriesMetadata.ExternalRatings + .Where(p => p.SeriesId == s.Id).Average(p => p.AverageScore), filter.SortOptions), _ => query }; @@ -1003,7 +1082,9 @@ public class SeriesRepository : ISeriesRepository var wantToReadStmt = filter.Statements.FirstOrDefault(stmt => stmt.Field == FilterField.WantToRead); if (wantToReadStmt == null) return query; - var seriesIds = _context.AppUser.Where(u => u.Id == userId).SelectMany(u => u.WantToRead).Select(s => s.Id); + var seriesIds = _context.AppUser.Where(u => u.Id == userId) + .SelectMany(u => u.WantToRead) + .Select(s => s.SeriesId); if (bool.Parse(wantToReadStmt.Value)) { query = query.Where(s => seriesIds.Contains(s.Id)); @@ -1116,6 +1197,7 @@ public class SeriesRepository : ISeriesRepository FilterField.ReleaseYear => query.HasReleaseYear(true, statement.Comparison, (int) value), FilterField.ReadTime => query.HasAverageReadTime(true, statement.Comparison, (int) value), FilterField.ReadingDate => query.HasReadingDate(true, statement.Comparison, (DateTime) value, userId), + FilterField.AverageRating => query.HasAverageRating(true, statement.Comparison, (float) value), _ => throw new ArgumentOutOfRangeException() }; } @@ -1223,8 +1305,10 @@ public class SeriesRepository : ISeriesRepository .Where(library => library.AppUsers.Any(x => x.Id == userId)) .AsSplitQuery() .Select(l => l.Id); + var userRating = await _context.AppUser.GetUserAgeRestriction(userId); return await _context.Series + .RestrictAgainstAgeRestriction(userRating) .Where(s => seriesIds.Contains(s.Id) && allowedLibraries.Contains(s.LibraryId)) .OrderBy(s => s.SortName.ToLower()) .ProjectTo(_mapper.ConfigurationProvider) @@ -1563,6 +1647,27 @@ public class SeriesRepository : ISeriesRepository #nullable enable } + public async Task> GetAllSeriesByAnyName(string seriesName, string localizedName, int libraryId, + MangaFormat format) + { + var normalizedSeries = seriesName.ToNormalized(); + var normalizedLocalized = localizedName.ToNormalized(); + return await _context.Series + .Where(s => s.LibraryId == libraryId) + .Where(s => s.Format == format && format != MangaFormat.Unknown) + .Where(s => + s.NormalizedName.Equals(normalizedSeries) + || s.NormalizedName.Equals(normalizedLocalized) + + || s.NormalizedLocalizedName.Equals(normalizedSeries) + || (!string.IsNullOrEmpty(normalizedLocalized) && s.NormalizedLocalizedName.Equals(normalizedLocalized)) + + || (s.OriginalName != null && s.OriginalName.Equals(seriesName)) + ) + .AsSplitQuery() + .ToListAsync(); + } + /// /// Removes series that are not in the seenSeries list. Does not commit. @@ -1789,7 +1894,7 @@ public class SeriesRepository : ISeriesRepository ChapterNumber = c.Number, ChapterRange = c.Range, IsSpecial = c.IsSpecial, - VolumeNumber = c.Volume.Number, + VolumeNumber = c.Volume.MinNumber, ChapterTitle = c.Title, AgeRating = c.Volume.Series.Metadata.AgeRating }) @@ -1805,7 +1910,8 @@ public class SeriesRepository : ISeriesRepository var query = _context.AppUser .Where(user => user.Id == userId) .SelectMany(u => u.WantToRead) - .Where(s => libraryIds.Contains(s.LibraryId)) + .Where(s => libraryIds.Contains(s.Series.LibraryId)) + .Select(w => w.Series) .AsSplitQuery() .AsNoTracking(); @@ -1820,7 +1926,8 @@ public class SeriesRepository : ISeriesRepository var query = _context.AppUser .Where(user => user.Id == userId) .SelectMany(u => u.WantToRead) - .Where(s => libraryIds.Contains(s.LibraryId)) + .Where(s => libraryIds.Contains(s.Series.LibraryId)) + .Select(w => w.Series) .AsSplitQuery() .AsNoTracking(); @@ -1835,28 +1942,33 @@ public class SeriesRepository : ISeriesRepository return await _context.AppUser .Where(user => user.Id == userId) .SelectMany(u => u.WantToRead) - .Where(s => libraryIds.Contains(s.LibraryId)) + .Where(s => libraryIds.Contains(s.Series.LibraryId)) + .Select(w => w.Series) .AsSplitQuery() .AsNoTracking() .ToListAsync(); } /// - /// Uses multiple names to find a match against a series then ensures the user has appropriate access to it. If not, returns null. + /// Uses multiple names to find a match against a series. If not, returns null. /// + /// This does not restrict to the user at all. That is handled at the API level. /// /// /// - public async Task GetSeriesDtoByNamesAndMetadataIdsForUser(int userId, IEnumerable names, LibraryType libraryType, string aniListUrl, string malUrl) + public async Task GetSeriesDtoByNamesAndMetadataIds(IEnumerable names, LibraryType libraryType, string aniListUrl, string malUrl) { - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); - var libraryIds = await _context.Library.GetUserLibrariesByType(userId, libraryType).ToListAsync(); + var libraryIds = await _context.Library + .Where(lib => lib.Type == libraryType) + .Select(l => l.Id) + .ToListAsync(); + var normalizedNames = names.Select(n => n.ToNormalized()).ToList(); SeriesDto? result = null; if (!string.IsNullOrEmpty(aniListUrl) || !string.IsNullOrEmpty(malUrl)) { + // TODO: I can likely work AniList and MalIds from ExternalSeriesMetadata in here result = await _context.Series - .RestrictAgainstAgeRestriction(userRating) .Where(s => !string.IsNullOrEmpty(s.Metadata.WebLinks)) .Where(s => libraryIds.Contains(s.Library.Id)) .WhereIf(!string.IsNullOrEmpty(aniListUrl), s => s.Metadata.WebLinks.Contains(aniListUrl)) @@ -1869,7 +1981,6 @@ public class SeriesRepository : ISeriesRepository if (result != null) return result; return await _context.Series - .RestrictAgainstAgeRestriction(userRating) .Where(s => normalizedNames.Contains(s.NormalizedName) || normalizedNames.Contains(s.NormalizedLocalizedName)) .Where(s => libraryIds.Contains(s.Library.Id)) @@ -1886,7 +1997,8 @@ public class SeriesRepository : ISeriesRepository { // If there is 0 or 1 rating and that rating is you, return 0 back var countOfRatingsThatAreUser = await _context.AppUserRating - .Where(r => r.SeriesId == seriesId && r.HasBeenRated).CountAsync(u => u.AppUserId == userId); + .Where(r => r.SeriesId == seriesId && r.HasBeenRated) + .CountAsync(u => u.AppUserId == userId); if (countOfRatingsThatAreUser == 1) { return 0; @@ -1926,7 +2038,7 @@ public class SeriesRepository : ISeriesRepository var libraryIds = await _context.Library.GetUserLibraries(userId).ToListAsync(); return await _context.AppUser .Where(user => user.Id == userId) - .SelectMany(u => u.WantToRead.Where(s => s.Id == seriesId && libraryIds.Contains(s.LibraryId))) + .SelectMany(u => u.WantToRead.Where(s => s.SeriesId == seriesId && libraryIds.Contains(s.Series.LibraryId))) .AsSplitQuery() .AsNoTracking() .AnyAsync(); diff --git a/API/Data/Repositories/SettingsRepository.cs b/API/Data/Repositories/SettingsRepository.cs index f142947b1..6d67b36b5 100644 --- a/API/Data/Repositories/SettingsRepository.cs +++ b/API/Data/Repositories/SettingsRepository.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using API.DTOs.SeriesDetail; using API.DTOs.Settings; using API.Entities; using API.Entities.Enums; +using API.Entities.Metadata; using AutoMapper; using Microsoft.EntityFrameworkCore; @@ -16,6 +18,7 @@ public interface ISettingsRepository Task GetSettingAsync(ServerSettingKey key); Task> GetSettingsAsync(); void Remove(ServerSetting setting); + Task GetExternalSeriesMetadata(int seriesId); } public class SettingsRepository : ISettingsRepository { @@ -38,6 +41,13 @@ public class SettingsRepository : ISettingsRepository _context.Remove(setting); } + public async Task GetExternalSeriesMetadata(int seriesId) + { + return await _context.ExternalSeriesMetadata + .Where(s => s.SeriesId == seriesId) + .FirstOrDefaultAsync(); + } + public async Task GetSettingsDtoAsync() { var settings = await _context.ServerSetting diff --git a/API/Data/Repositories/VolumeRepository.cs b/API/Data/Repositories/VolumeRepository.cs index ccd909117..53a45a946 100644 --- a/API/Data/Repositories/VolumeRepository.cs +++ b/API/Data/Repositories/VolumeRepository.cs @@ -152,7 +152,7 @@ public class VolumeRepository : IVolumeRepository .Include(vol => vol.Chapters) .ThenInclude(c => c.Files) .AsSplitQuery() - .OrderBy(vol => vol.Number) + .OrderBy(vol => vol.MinNumber) .ToListAsync(); } @@ -185,7 +185,7 @@ public class VolumeRepository : IVolumeRepository .ThenInclude(c => c.People) .Include(vol => vol.Chapters) .ThenInclude(c => c.Tags) - .OrderBy(volume => volume.Number) + .OrderBy(volume => volume.MinNumber) .ProjectTo(_mapper.ConfigurationProvider) .AsNoTracking() .AsSplitQuery() @@ -215,7 +215,7 @@ public class VolumeRepository : IVolumeRepository private static void SortSpecialChapters(IEnumerable volumes) { - foreach (var v in volumes.Where(vDto => vDto.Number == 0)) + foreach (var v in volumes.Where(vDto => vDto.MinNumber == 0)) { v.Chapters = v.Chapters.OrderByNatural(x => x.Range).ToList(); } @@ -241,7 +241,9 @@ public class VolumeRepository : IVolumeRepository c.LastReadingProgress = progresses.Max(p => p.LastModified); } - v.PagesRead = userProgress.Where(p => p.VolumeId == v.Id).Sum(p => p.PagesRead); + v.PagesRead = userProgress + .Where(p => p.VolumeId == v.Id) + .Sum(p => p.PagesRead); } } } diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs index c5fe643ea..ac1cfb1f1 100644 --- a/API/Data/Seed.cs +++ b/API/Data/Seed.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; @@ -208,8 +207,9 @@ public static class Seed { new() {Key = ServerSettingKey.CacheDirectory, Value = directoryService.CacheDirectory}, new() {Key = ServerSettingKey.TaskScan, Value = "daily"}, - new() {Key = ServerSettingKey.LoggingLevel, Value = "Debug"}, new() {Key = ServerSettingKey.TaskBackup, Value = "daily"}, + new() {Key = ServerSettingKey.TaskCleanup, Value = "daily"}, + new() {Key = ServerSettingKey.LoggingLevel, Value = "Debug"}, new() { Key = ServerSettingKey.BackupDirectory, Value = Path.GetFullPath(DirectoryService.BackupDirectory) @@ -223,12 +223,10 @@ public static class Seed }, // Not used from DB, but DB is sync with appSettings.json new() {Key = ServerSettingKey.AllowStatCollection, Value = "true"}, new() {Key = ServerSettingKey.EnableOpds, Value = "true"}, - new() {Key = ServerSettingKey.EnableAuthentication, Value = "true"}, new() {Key = ServerSettingKey.BaseUrl, Value = "/"}, new() {Key = ServerSettingKey.InstallId, Value = HashUtil.AnonymousToken()}, new() {Key = ServerSettingKey.InstallVersion, Value = BuildInfo.Version.ToString()}, new() {Key = ServerSettingKey.BookmarkDirectory, Value = directoryService.BookmarkDirectory}, - new() {Key = ServerSettingKey.EmailServiceUrl, Value = EmailService.DefaultApiUrl}, new() {Key = ServerSettingKey.TotalBackups, Value = "30"}, new() {Key = ServerSettingKey.TotalLogs, Value = "30"}, new() {Key = ServerSettingKey.EnableFolderWatching, Value = "false"}, @@ -241,6 +239,16 @@ public static class Seed new() { Key = ServerSettingKey.CacheSize, Value = Configuration.DefaultCacheMemory + string.Empty }, // Not used from DB, but DB is sync with appSettings.json + + new() {Key = ServerSettingKey.EmailHost, Value = string.Empty}, + new() {Key = ServerSettingKey.EmailPort, Value = string.Empty}, + new() {Key = ServerSettingKey.EmailAuthPassword, Value = string.Empty}, + new() {Key = ServerSettingKey.EmailAuthUserName, Value = string.Empty}, + new() {Key = ServerSettingKey.EmailSenderAddress, Value = string.Empty}, + new() {Key = ServerSettingKey.EmailSenderDisplayName, Value = string.Empty}, + new() {Key = ServerSettingKey.EmailEnableSsl, Value = "true"}, + new() {Key = ServerSettingKey.EmailSizeLimit, Value = 26_214_400 + string.Empty}, + new() {Key = ServerSettingKey.EmailCustomizedTemplates, Value = "false"}, }.ToArray()); foreach (var defaultSetting in DefaultSettings) diff --git a/API/Data/UnitOfWork.cs b/API/Data/UnitOfWork.cs index 49178cc57..97ef3e07b 100644 --- a/API/Data/UnitOfWork.cs +++ b/API/Data/UnitOfWork.cs @@ -30,6 +30,7 @@ public interface IUnitOfWork IUserTableOfContentRepository UserTableOfContentRepository { get; } IAppUserSmartFilterRepository AppUserSmartFilterRepository { get; } IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; } + IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; } bool Commit(); Task CommitAsync(); bool HasChanges(); @@ -48,7 +49,7 @@ public class UnitOfWork : IUnitOfWork _userManager = userManager; } - public ISeriesRepository SeriesRepository => new SeriesRepository(_context, _mapper); + public ISeriesRepository SeriesRepository => new SeriesRepository(_context, _mapper, _userManager); public IUserRepository UserRepository => new UserRepository(_context, _userManager, _mapper); public ILibraryRepository LibraryRepository => new LibraryRepository(_context, _mapper); @@ -72,6 +73,7 @@ public class UnitOfWork : IUnitOfWork public IUserTableOfContentRepository UserTableOfContentRepository => new UserTableOfContentRepository(_context, _mapper); public IAppUserSmartFilterRepository AppUserSmartFilterRepository => new AppUserSmartFilterRepository(_context, _mapper); public IAppUserExternalSourceRepository AppUserExternalSourceRepository => new AppUserExternalSourceRepository(_context, _mapper); + public IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository => new ExternalSeriesMetadataRepository(_context, _mapper); /// /// Commits changes to the DB. Completes the open transaction. diff --git a/API/EmailTemplates/EmailChange.html b/API/EmailTemplates/EmailChange.html new file mode 100644 index 000000000..6423e024f --- /dev/null +++ b/API/EmailTemplates/EmailChange.html @@ -0,0 +1,27 @@ + + +

Email Change Update

+

Your account's email has been updated on {{InvitingUser}}'s Kavita instance.

+

Please click the following link to validate your email change. The email is not changed until you complete validation.

+ + + + + + + + + + +

If the button above does not work, please find the link here: {{Link}}

+ + + diff --git a/API/EmailTemplates/EmailConfirm.html b/API/EmailTemplates/EmailConfirm.html new file mode 100644 index 000000000..194f88ec8 --- /dev/null +++ b/API/EmailTemplates/EmailConfirm.html @@ -0,0 +1,26 @@ + + +

You've Been Invited!

+

You have been invited to {{InvitingUser}}'s Kavita instance.

+

Please click the following link to setup an account for yourself and start reading.

+ + + + + + + + + +

If the button above does not work, please find the link here: {{Link}}

+ + + diff --git a/API/EmailTemplates/EmailPasswordReset.html b/API/EmailTemplates/EmailPasswordReset.html new file mode 100644 index 000000000..2518ffc53 --- /dev/null +++ b/API/EmailTemplates/EmailPasswordReset.html @@ -0,0 +1,27 @@ + + +

Forgot your password?

+

That's okay, it happens! Click on the button below to reset your password.

+ +

If you did not perform this action, ignore this email. Your account is safe.

+ + + + + + + + + +

If the button above does not work, please find the link here: {{Link}}

+ + + diff --git a/API/EmailTemplates/EmailTest.html b/API/EmailTemplates/EmailTest.html new file mode 100644 index 000000000..ae00e4889 --- /dev/null +++ b/API/EmailTemplates/EmailTest.html @@ -0,0 +1,8 @@ + + +

This is a Test Email

+

Congrats! Your instance of Kavita is setup to email correctly!

+ +

If you did not perform this action, ignore this email. Your account is safe.

+ + diff --git a/API/EmailTemplates/SendToDevice.html b/API/EmailTemplates/SendToDevice.html new file mode 100644 index 000000000..673d773c5 --- /dev/null +++ b/API/EmailTemplates/SendToDevice.html @@ -0,0 +1,6 @@ + + +

You sent file(s) from Kavita

+

Please find attached the file(s) you've sent.

+ + diff --git a/API/EmailTemplates/base.html b/API/EmailTemplates/base.html new file mode 100644 index 000000000..198a1b211 --- /dev/null +++ b/API/EmailTemplates/base.html @@ -0,0 +1,527 @@ + + + + + + + + + + + + + + + + + + Kavita - {{Preheader}} + + + + + +
{{Preheader}}
+ + + + + + + + diff --git a/API/Entities/AppUser.cs b/API/Entities/AppUser.cs index 62c8cc81a..f87531e8a 100644 --- a/API/Entities/AppUser.cs +++ b/API/Entities/AppUser.cs @@ -31,7 +31,7 @@ public class AppUser : IdentityUser, IHasConcurrencyToken /// /// A list of Series the user want's to read /// - public ICollection WantToRead { get; set; } = null!; + public ICollection WantToRead { get; set; } = null!; /// /// A list of Devices which allows the user to send files to /// diff --git a/API/Entities/AppUserWantToRead.cs b/API/Entities/AppUserWantToRead.cs new file mode 100644 index 000000000..d41e44962 --- /dev/null +++ b/API/Entities/AppUserWantToRead.cs @@ -0,0 +1,20 @@ +namespace API.Entities; + +public class AppUserWantToRead +{ + public int Id { get; set; } + + public required int SeriesId { get; set; } + public virtual Series Series { get; set; } + + + // Relationships + /// + /// Navigational Property for EF. Links to a unique AppUser + /// + public AppUser AppUser { get; set; } = null!; + /// + /// User this table of content belongs to + /// + public int AppUserId { get; set; } +} diff --git a/API/Entities/Enums/LibraryType.cs b/API/Entities/Enums/LibraryType.cs index 7206d423a..2be2f9559 100644 --- a/API/Entities/Enums/LibraryType.cs +++ b/API/Entities/Enums/LibraryType.cs @@ -25,8 +25,13 @@ public enum LibraryType [Description("Image")] Image = 3, /// + /// Allows Books to Scrobble with AniList for Kavita+ + /// + [Description("Light Novel")] + LightNovel = 4, + /// /// Uses Magazine regex and is restricted to PDF and Archive by default /// [Description("Magazine")] - Magazine = 4 + Magazine = 5 } diff --git a/API/Entities/Enums/ServerSettingKey.cs b/API/Entities/Enums/ServerSettingKey.cs index af699a3d9..be53a105d 100644 --- a/API/Entities/Enums/ServerSettingKey.cs +++ b/API/Entities/Enums/ServerSettingKey.cs @@ -52,6 +52,7 @@ public enum ServerSettingKey /// Is Authentication needed for non-admin accounts ///
/// Deprecated. This is no longer used v0.5.1+. Assume Authentication is always in effect + [Obsolete("Not supported as of v0.5.1")] [Description("EnableAuthentication")] EnableAuthentication = 8, /// @@ -79,6 +80,7 @@ public enum ServerSettingKey /// If SMTP is enabled on the server /// [Description("CustomEmailService")] + [Obsolete("Use Email settings instead")] EmailServiceUrl = 13, /// /// If Kavita should save bookmarks as WebP images @@ -147,6 +149,42 @@ public enum ServerSettingKey /// The size of the cover image thumbnail. Defaults to .Default /// [Description("CoverImageSize")] - CoverImageSize = 27 - + CoverImageSize = 27, + #region EmailSettings + /// + /// The address of the emailer host + /// + [Description("EmailSenderAddress")] + EmailSenderAddress = 28, + /// + /// What the email name should be + /// + [Description("EmailSenderDisplayName")] + EmailSenderDisplayName = 29, + [Description("EmailAuthUserName")] + EmailAuthUserName = 30, + [Description("EmailAuthPassword")] + EmailAuthPassword = 31, + [Description("EmailHost")] + EmailHost = 32, + [Description("EmailPort")] + EmailPort = 33, + [Description("EmailEnableSsl")] + EmailEnableSsl = 34, + /// + /// Number of bytes that the sender allows to be sent through + /// + [Description("EmailSizeLimit")] + EmailSizeLimit = 35, + /// + /// Should Kavita use config/templates for Email templates or the default ones + /// + [Description("EmailCustomizedTemplates")] + EmailCustomizedTemplates = 36, + #endregion + /// + /// When the cleanup task should run - Critical to keeping Kavita working + /// + [Description("TaskCleanup")] + TaskCleanup = 37 } diff --git a/API/Entities/ManualMigrationHistory.cs b/API/Entities/ManualMigrationHistory.cs new file mode 100644 index 000000000..e65e07b2c --- /dev/null +++ b/API/Entities/ManualMigrationHistory.cs @@ -0,0 +1,14 @@ +using System; + +namespace API.Entities; + +/// +/// This will track manual migrations so that I can use simple selects to check if a Manual Migration is needed +/// +public class ManualMigrationHistory +{ + public int Id { get; set; } + public string ProductVersion { get; set; } + public required string Name { get; set; } + public DateTime RanAt { get; set; } +} diff --git a/API/Entities/Metadata/ExternalRating.cs b/API/Entities/Metadata/ExternalRating.cs new file mode 100644 index 000000000..b325353e4 --- /dev/null +++ b/API/Entities/Metadata/ExternalRating.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using API.Services.Plus; + +namespace API.Entities.Metadata; + +public class ExternalRating +{ + public int Id { get; set; } + + public int AverageScore { get; set; } + public int FavoriteCount { get; set; } + public ScrobbleProvider Provider { get; set; } + public string? ProviderUrl { get; set; } + public int SeriesId { get; set; } + + public ICollection ExternalSeriesMetadatas { get; set; } = null!; +} diff --git a/API/Entities/Metadata/ExternalRecommendation.cs b/API/Entities/Metadata/ExternalRecommendation.cs new file mode 100644 index 000000000..c5bb98f20 --- /dev/null +++ b/API/Entities/Metadata/ExternalRecommendation.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using API.Services.Plus; +using Microsoft.EntityFrameworkCore; + + +namespace API.Entities.Metadata; + +[Index(nameof(SeriesId), IsUnique = false)] +public class ExternalRecommendation +{ + public int Id { get; set; } + + public required string Name { get; set; } + public required string CoverUrl { get; set; } + public required string Url { get; set; } + public string? Summary { get; set; } + public int? AniListId { get; set; } + public long? MalId { get; set; } + public ScrobbleProvider Provider { get; set; } = ScrobbleProvider.AniList; + + /// + /// When null, represents an external series. When set, it is a Series + /// + public int? SeriesId { get; set; } + //public virtual Series? Series { get; set; } + + // Relationships + public ICollection ExternalSeriesMetadatas { get; set; } = null!; +} diff --git a/API/Entities/Metadata/ExternalReview.cs b/API/Entities/Metadata/ExternalReview.cs new file mode 100644 index 000000000..6304d98ad --- /dev/null +++ b/API/Entities/Metadata/ExternalReview.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using API.Services.Plus; + +namespace API.Entities.Metadata; + +/// +/// Represents an Externally supplied Review for a given Series +/// +public class ExternalReview +{ + public int Id { get; set; } + public string Tagline { get; set; } + public required string Body { get; set; } + /// + /// Pure text version of the body + /// + public required string BodyJustText { get; set; } + /// + /// Raw from the provider. Usually Markdown + /// + public string RawBody { get; set; } + public required ScrobbleProvider Provider { get; set; } + public string SiteUrl { get; set; } + /// + /// Reviewer's username + /// + public string Username { get; set; } + /// + /// An Optional Rating coming from the Review + /// + public int Rating { get; set; } = 0; + /// + /// The media's overall Score + /// + public int Score { get; set; } + public int TotalVotes { get; set; } + + + public int SeriesId { get; set; } + + // Relationships + public ICollection ExternalSeriesMetadatas { get; set; } = null!; +} diff --git a/API/Entities/Metadata/ExternalSeriesMetadata.cs b/API/Entities/Metadata/ExternalSeriesMetadata.cs new file mode 100644 index 000000000..215a01585 --- /dev/null +++ b/API/Entities/Metadata/ExternalSeriesMetadata.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; + +namespace API.Entities.Metadata; + +/// +/// External Metadata from Kavita+ for a Series +/// +public class ExternalSeriesMetadata +{ + public int Id { get; set; } + /// + /// External Reviews for the Series. Managed by Kavita for Kavita+ users + /// + public ICollection ExternalReviews { get; set; } = null!; + public ICollection ExternalRatings { get; set; } = null!; + /// + /// External recommendations will include all recommendations and will have a seriesId if it's on this Kavita instance. + /// + /// Cleanup Service will perform matching to tie new series with recommendations + public ICollection ExternalRecommendations { get; set; } = null!; + + /// + /// Average External Rating. -1 means not set + /// + public int AverageExternalRating { get; set; } = 0; + + public int AniListId { get; set; } + public long MalId { get; set; } + public string GoogleBooksId { get; set; } + + /// + /// Data is valid until this time + /// + public DateTime ValidUntilUtc { get; set; } + + public Series Series { get; set; } = null!; + public int SeriesId { get; set; } +} diff --git a/API/Entities/Metadata/SeriesBlacklist.cs b/API/Entities/Metadata/SeriesBlacklist.cs new file mode 100644 index 000000000..09ff06153 --- /dev/null +++ b/API/Entities/Metadata/SeriesBlacklist.cs @@ -0,0 +1,14 @@ +using System; + +namespace API.Entities.Metadata; + +/// +/// A blacklist of Series for Kavita+ +/// +public class SeriesBlacklist +{ + public int Id { get; set; } + public int SeriesId { get; set; } + public Series Series { get; set; } + public DateTime LastChecked { get; set; } = DateTime.UtcNow; +} diff --git a/API/Entities/Scrobble/ScrobbleEvent.cs b/API/Entities/Scrobble/ScrobbleEvent.cs index 2fd36eef3..a02363992 100644 --- a/API/Entities/Scrobble/ScrobbleEvent.cs +++ b/API/Entities/Scrobble/ScrobbleEvent.cs @@ -36,12 +36,20 @@ public class ScrobbleEvent : IEntityDate /// /// Depends on the ScrobbleEvent if filled in /// - public int? VolumeNumber { get; set; } + public float? VolumeNumber { get; set; } /// /// Has this event been processed and pushed to Provider /// public bool IsProcessed { get; set; } /// + /// Was there an error processing this event + /// + public bool IsErrored { get; set; } + /// + /// The error details + /// + public string? ErrorDetails { get; set; } + /// /// The date this was processed /// public DateTime? ProcessDateUtc { get; set; } diff --git a/API/Entities/Scrobble/ScrobbleEventFilter.cs b/API/Entities/Scrobble/ScrobbleEventFilter.cs index 526843b68..1153e90e9 100644 --- a/API/Entities/Scrobble/ScrobbleEventFilter.cs +++ b/API/Entities/Scrobble/ScrobbleEventFilter.cs @@ -15,4 +15,8 @@ public class ScrobbleEventFilter /// A query to search against ///
public string Query { get; set; } + /// + /// Include reviews in the result - Note: Review Scrobbling is disabled + /// + public bool IncludeReviews { get; set; } = false; } diff --git a/API/Entities/Series.cs b/API/Entities/Series.cs index e4403eb84..311255aec 100644 --- a/API/Entities/Series.cs +++ b/API/Entities/Series.cs @@ -96,6 +96,7 @@ public class Series : IEntityDate, IHasReadTimeEstimate public int AvgHoursToRead { get; set; } public SeriesMetadata Metadata { get; set; } = null!; + public ExternalSeriesMetadata ExternalSeriesMetadata { get; set; } = null!; public ICollection Ratings { get; set; } = null!; public ICollection Progress { get; set; } = null!; @@ -113,6 +114,7 @@ public class Series : IEntityDate, IHasReadTimeEstimate public Library Library { get; set; } = null!; public int LibraryId { get; set; } + public void UpdateLastFolderScanned() { LastFolderScanned = DateTime.Now; diff --git a/API/Entities/Volume.cs b/API/Entities/Volume.cs index f5239e708..2a2e4b29a 100644 --- a/API/Entities/Volume.cs +++ b/API/Entities/Volume.cs @@ -8,14 +8,24 @@ public class Volume : IEntityDate, IHasReadTimeEstimate { public int Id { get; set; } /// - /// A String representation of the volume number. Allows for floats. + /// A String representation of the volume number. Allows for floats. Can also include a range (1-2). /// /// For Books with Series_index, this will map to the Series Index. public required string Name { get; set; } /// /// The minimum number in the Name field in Int form /// - public required int Number { get; set; } + /// Removed in v0.7.13.8, this was an int and we need the ability to have 0.5 volumes render on the UI + [Obsolete("Use MinNumber and MaxNumber instead")] + public int Number { get; set; } + /// + /// The minimum number in the Name field + /// + public required float MinNumber { get; set; } + /// + /// The maximum number in the Name field (same as Minimum if Name isn't a range) + /// + public required float MaxNumber { get; set; } public IList Chapters { get; set; } = null!; public DateTime Created { get; set; } public DateTime LastModified { get; set; } diff --git a/API/Errors/ApiException.cs b/API/Errors/ApiException.cs index d9c1a755a..60d93729c 100644 --- a/API/Errors/ApiException.cs +++ b/API/Errors/ApiException.cs @@ -2,4 +2,3 @@ #nullable enable public record ApiException(int Status, string? Message = null, string? Details = null); -#nullable disable diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index d7a30415a..104bb4fe1 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -72,8 +72,6 @@ public static class ApplicationServiceExtensions services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddSqLite(); @@ -85,18 +83,14 @@ public static class ApplicationServiceExtensions options.UseInMemory(EasyCacheProfiles.License); options.UseInMemory(EasyCacheProfiles.Library); options.UseInMemory(EasyCacheProfiles.RevokedJwt); - options.UseInMemory(EasyCacheProfiles.Filter); // KavitaPlus stuff - options.UseInMemory(EasyCacheProfiles.KavitaPlusReviews); - options.UseInMemory(EasyCacheProfiles.KavitaPlusRecommendations); - options.UseInMemory(EasyCacheProfiles.KavitaPlusRatings); options.UseInMemory(EasyCacheProfiles.KavitaPlusExternalSeries); }); services.AddMemoryCache(options => { - options.SizeLimit = Configuration.CacheSize * 1024 * 1024; // 50 MB + options.SizeLimit = Configuration.CacheSize * 1024 * 1024; // 75 MB options.CompactionPercentage = 0.1; // LRU compaction (10%) }); @@ -110,9 +104,11 @@ public static class ApplicationServiceExtensions { services.AddDbContextPool(options => { - options.UseSqlite("Data source=config/kavita.db"); + options.UseSqlite("Data source=config/kavita.db", builder => + { + builder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); + }); options.EnableDetailedErrors(); - options.EnableSensitiveDataLogging(); }); } diff --git a/API/Extensions/QueryExtensions/Filtering/BookmarkSort.cs b/API/Extensions/QueryExtensions/Filtering/BookmarkSort.cs index 1ef2d5dd8..f3dbfef14 100644 --- a/API/Extensions/QueryExtensions/Filtering/BookmarkSort.cs +++ b/API/Extensions/QueryExtensions/Filtering/BookmarkSort.cs @@ -37,6 +37,8 @@ public static class BookmarkSort SortField.TimeToRead => query.DoOrderBy(s => s.Series.AvgHoursToRead, sortOptions), SortField.ReleaseYear => query.DoOrderBy(s => s.Series.Metadata.ReleaseYear, sortOptions), SortField.ReadProgress => query.DoOrderBy(s => s.Series.Progress.Where(p => p.SeriesId == s.Series.Id).Select(p => p.LastModified).Max(), sortOptions), + SortField.AverageRating => query.DoOrderBy(s => s.Series.ExternalSeriesMetadata.ExternalRatings + .Where(p => p.SeriesId == s.Series.Id).Average(p => p.AverageScore), sortOptions), _ => query }; diff --git a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs b/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs index 5ac6dc302..4a04d29a8 100644 --- a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs +++ b/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs @@ -246,6 +246,7 @@ public static class SeriesFilter .Where(p => p != null && p.AppUserId == userId) .Sum(p => p != null ? (p.PagesRead * 1.0f / s.Pages) : 0) * 100) }) + .AsSplitQuery() .AsEnumerable(); switch (comparison) @@ -287,6 +288,61 @@ public static class SeriesFilter return queryable.Where(s => ids.Contains(s.Id)); } + public static IQueryable HasAverageRating(this IQueryable queryable, bool condition, + FilterComparison comparison, float rating) + { + if (!condition) return queryable; + + var subQuery = queryable + .Where(s => s.ExternalSeriesMetadata != null) + .Include(s => s.ExternalSeriesMetadata) + .Select(s => new + { + Series = s, + AverageRating = s.ExternalSeriesMetadata.AverageExternalRating + }) + .AsSplitQuery() + .AsEnumerable(); + + switch (comparison) + { + case FilterComparison.Equal: + subQuery = subQuery.Where(s => Math.Abs(s.AverageRating - rating) < FloatingPointTolerance); + break; + case FilterComparison.GreaterThan: + subQuery = subQuery.Where(s => s.AverageRating > rating); + break; + case FilterComparison.GreaterThanEqual: + subQuery = subQuery.Where(s => s.AverageRating >= rating); + break; + case FilterComparison.LessThan: + subQuery = subQuery.Where(s => s.AverageRating < rating); + break; + case FilterComparison.LessThanEqual: + subQuery = subQuery.Where(s => s.AverageRating <= rating); + break; + case FilterComparison.NotEqual: + subQuery = subQuery.Where(s => Math.Abs(s.AverageRating - rating) > FloatingPointTolerance); + break; + case FilterComparison.Matches: + case FilterComparison.Contains: + case FilterComparison.NotContains: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: + throw new KavitaException($"{comparison} not applicable for Series.AverageRating"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + + var ids = subQuery.Select(s => s.Series.Id).ToList(); + return queryable.Where(s => ids.Contains(s.Id)); + } + public static IQueryable HasReadingDate(this IQueryable queryable, bool condition, FilterComparison comparison, DateTime? date, int userId) { @@ -304,6 +360,7 @@ public static class SeriesFilter .Max() }) .Where(s => s.MaxDate != null) + .AsSplitQuery() .AsEnumerable(); switch (comparison) diff --git a/API/Extensions/QueryExtensions/Filtering/SeriesSort.cs b/API/Extensions/QueryExtensions/Filtering/SeriesSort.cs index e59e9e922..4913c4059 100644 --- a/API/Extensions/QueryExtensions/Filtering/SeriesSort.cs +++ b/API/Extensions/QueryExtensions/Filtering/SeriesSort.cs @@ -33,6 +33,8 @@ public static class SeriesSort SortField.ReadProgress => query.DoOrderBy(s => s.Progress.Where(p => p.SeriesId == s.Id && p.AppUserId == userId) .Select(p => p.LastModified) .Max(), sortOptions), + SortField.AverageRating => query.DoOrderBy(s => s.ExternalSeriesMetadata.ExternalRatings + .Where(p => p.SeriesId == s.Id).Average(p => p.AverageScore), sortOptions), _ => query }; diff --git a/API/Extensions/QueryExtensions/IncludesExtensions.cs b/API/Extensions/QueryExtensions/IncludesExtensions.cs index a3afb765d..1250adeae 100644 --- a/API/Extensions/QueryExtensions/IncludesExtensions.cs +++ b/API/Extensions/QueryExtensions/IncludesExtensions.cs @@ -36,7 +36,6 @@ public static class IncludesExtensions .Include(c => c.Files); } - return queryable.AsSplitQuery(); } @@ -67,6 +66,33 @@ public static class IncludesExtensions .Include(s => s.RelationOf); } + if (includeFlags.HasFlag(SeriesIncludes.ExternalReviews)) + { + query = query + .Include(s => s.ExternalSeriesMetadata) + .ThenInclude(s => s.ExternalReviews); + } + + if (includeFlags.HasFlag(SeriesIncludes.ExternalRatings)) + { + query = query + .Include(s => s.ExternalSeriesMetadata) + .ThenInclude(s => s.ExternalRatings); + } + + if (includeFlags.HasFlag(SeriesIncludes.ExternalMetadata)) + { + query = query + .Include(s => s.ExternalSeriesMetadata); + } + + if (includeFlags.HasFlag(SeriesIncludes.ExternalRecommendations)) + { + query = query + .Include(s => s.ExternalSeriesMetadata) + .ThenInclude(s => s.ExternalRecommendations); + } + if (includeFlags.HasFlag(SeriesIncludes.Metadata)) { query = query.Include(s => s.Metadata) diff --git a/API/Extensions/SeriesExtensions.cs b/API/Extensions/SeriesExtensions.cs index ba8bcc83e..5db96a30c 100644 --- a/API/Extensions/SeriesExtensions.cs +++ b/API/Extensions/SeriesExtensions.cs @@ -18,8 +18,8 @@ public static class SeriesExtensions /// This is under the assumption that the Volume already has a Cover Image calculated and set public static string? GetCoverImage(this Series series) { - var volumes = (series.Volumes ?? new List()) - .OrderBy(v => v.Number, ChapterSortComparer.Default) + var volumes = (series.Volumes ?? []) + .OrderBy(v => v.MinNumber, ChapterSortComparer.Default) .ToList(); var firstVolume = volumes.GetCoverImage(series.Format); if (firstVolume == null) return null; @@ -34,20 +34,20 @@ public static class SeriesExtensions } // just volumes - if (volumes.TrueForAll(v => $"{v.Number}" != Parser.DefaultVolume)) + if (volumes.TrueForAll(v => $"{v.MinNumber}" != Parser.DefaultVolume)) { return firstVolume.CoverImage; } // If we have loose leaf chapters // if loose leaf chapters AND volumes, just return first volume - if (volumes.Count >= 1 && $"{volumes[0].Number}" != Parser.DefaultVolume) + if (volumes.Count >= 1 && $"{volumes[0].MinNumber}" != Parser.DefaultVolume) { - var looseLeafChapters = volumes.Where(v => $"{v.Number}" == Parser.DefaultVolume) + var looseLeafChapters = volumes.Where(v => $"{v.MinNumber}" == Parser.DefaultVolume) .SelectMany(c => c.Chapters.Where(c => !c.IsSpecial)) .OrderBy(c => c.Number.AsDouble(), ChapterSortComparerZeroFirst.Default) .ToList(); - if (looseLeafChapters.Count > 0 && (1.0f * volumes[0].Number) > looseLeafChapters[0].Number.AsFloat()) + if (looseLeafChapters.Count > 0 && (1.0f * volumes[0].MinNumber) > looseLeafChapters[0].Number.AsFloat()) { return looseLeafChapters[0].CoverImage; } @@ -55,7 +55,7 @@ public static class SeriesExtensions } var firstLooseLeafChapter = volumes - .Where(v => $"{v.Number}" == Parser.DefaultVolume) + .Where(v => $"{v.MinNumber}" == Parser.DefaultVolume) .SelectMany(v => v.Chapters) .OrderBy(c => c.Number.AsDouble(), ChapterSortComparerZeroFirst.Default) .FirstOrDefault(c => !c.IsSpecial); diff --git a/API/Extensions/VolumeListExtensions.cs b/API/Extensions/VolumeListExtensions.cs index 51dc5cf8c..f1ef051b1 100644 --- a/API/Extensions/VolumeListExtensions.cs +++ b/API/Extensions/VolumeListExtensions.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using API.Entities; using API.Entities.Enums; +using API.Services.Tasks.Scanner.Parser; namespace API.Extensions; #nullable enable @@ -22,16 +23,16 @@ public static class VolumeListExtensions if (seriesFormat == MangaFormat.Epub || seriesFormat == MangaFormat.Pdf) { - return volumes.MinBy(x => x.Number); + return volumes.MinBy(x => x.MinNumber); } - if (volumes.Any(x => x.Number != 0)) + if (volumes.Any(x => x.MinNumber != 0f)) // TODO: Refactor this so we can avoid a magic number { - return volumes.OrderBy(x => x.Number).FirstOrDefault(x => x.Number != 0); + return volumes.OrderBy(x => x.MinNumber).FirstOrDefault(x => x.MinNumber != 0); } // We only have 1 volume of chapters, we need to be cautious if there are specials, as we don't want to order them first - return volumes.MinBy(x => x.Number); + return volumes.MinBy(x => x.MinNumber); } } diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index 8d0a4fa43..3f96fd344 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -12,6 +12,7 @@ using API.DTOs.MediaErrors; using API.DTOs.Metadata; using API.DTOs.Reader; using API.DTOs.ReadingLists; +using API.DTOs.Recommendation; using API.DTOs.Scrobbling; using API.DTOs.Search; using API.DTOs.SeriesDetail; @@ -24,6 +25,7 @@ using API.Entities.Metadata; using API.Entities.Scrobble; using API.Extensions.QueryExtensions.Filtering; using API.Helpers.Converters; +using API.Services; using AutoMapper; using CollectionTag = API.Entities.CollectionTag; using MediaError = API.Entities.MediaError; @@ -44,7 +46,8 @@ public class AutoMapperProfiles : Profile .ForMember(dest => dest.ChapterId, opt => opt.MapFrom(src => src.Bookmark.ChapterId)) .ForMember(dest => dest.Series, opt => opt.MapFrom(src => src.Series)); CreateMap(); - CreateMap(); + CreateMap() + .ForMember(dest => dest.Number, opt => opt.MapFrom(src => src.MinNumber)); CreateMap(); CreateMap(); CreateMap(); @@ -245,5 +248,18 @@ public class AutoMapperProfiles : Profile CreateMap(); CreateMap(); + CreateMap(); + CreateMap(); + CreateMap() + .ForMember(dest => dest.IsExternal, + opt => + opt.MapFrom(src => true)); + + CreateMap() + .ForMember(dest => dest.BodyJustText, + opt => + opt.MapFrom(src => ReviewService.GetCharacters(src.Body))); + + CreateMap(); } } diff --git a/API/Helpers/Builders/LibraryBuilder.cs b/API/Helpers/Builders/LibraryBuilder.cs index ce392e78b..1cfd529a1 100644 --- a/API/Helpers/Builders/LibraryBuilder.cs +++ b/API/Helpers/Builders/LibraryBuilder.cs @@ -20,7 +20,7 @@ public class LibraryBuilder : IEntityBuilder Series = new List(), Folders = new List(), AppUsers = new List(), - AllowScrobbling = type is LibraryType.Book or LibraryType.Manga + AllowScrobbling = type is LibraryType.LightNovel or LibraryType.Manga }; } diff --git a/API/Helpers/Builders/PlusSeriesDtoBuilder.cs b/API/Helpers/Builders/PlusSeriesDtoBuilder.cs index f1c43df84..6a8e70bde 100644 --- a/API/Helpers/Builders/PlusSeriesDtoBuilder.cs +++ b/API/Helpers/Builders/PlusSeriesDtoBuilder.cs @@ -1,5 +1,6 @@ using System.Linq; using API.DTOs; +using API.DTOs.Scrobbling; using API.Entities; using API.Services.Plus; diff --git a/API/Helpers/Builders/SeriesBuilder.cs b/API/Helpers/Builders/SeriesBuilder.cs index 5d73dcd6d..5d5ce9f51 100644 --- a/API/Helpers/Builders/SeriesBuilder.cs +++ b/API/Helpers/Builders/SeriesBuilder.cs @@ -27,7 +27,8 @@ public class SeriesBuilder : IEntityBuilder NormalizedName = name.ToNormalized(), NormalizedLocalizedName = name.ToNormalized(), Metadata = new SeriesMetadataBuilder().Build(), - Volumes = new List() + Volumes = new List(), + ExternalSeriesMetadata = new ExternalSeriesMetadata() }; } diff --git a/API/Helpers/Builders/VolumeBuilder.cs b/API/Helpers/Builders/VolumeBuilder.cs index aabde6ce2..158a84bfa 100644 --- a/API/Helpers/Builders/VolumeBuilder.cs +++ b/API/Helpers/Builders/VolumeBuilder.cs @@ -15,8 +15,8 @@ public class VolumeBuilder : IEntityBuilder _volume = new Volume() { Name = volumeNumber, - // TODO / BUG: Try to use float based Number which will allow Epub's with < 1 volumes to show in series detail - Number = (int) Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(volumeNumber), + MinNumber = Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(volumeNumber), + MaxNumber = Services.Tasks.Scanner.Parser.Parser.MaxNumberFromRange(volumeNumber), Chapters = new List() }; } @@ -27,9 +27,25 @@ public class VolumeBuilder : IEntityBuilder return this; } - public VolumeBuilder WithNumber(int number) + public VolumeBuilder WithNumber(float number) { - _volume.Number = number; + _volume.MinNumber = number; + if (_volume.MaxNumber < number) + { + _volume.MaxNumber = number; + } + return this; + } + + public VolumeBuilder WithMinNumber(float number) + { + _volume.MinNumber = number; + return this; + } + + public VolumeBuilder WithMaxNumber(float number) + { + _volume.MaxNumber = number; return this; } diff --git a/API/Helpers/Converters/CronConverter.cs b/API/Helpers/Converters/CronConverter.cs index a16ce4ef3..f1f0ebc1b 100644 --- a/API/Helpers/Converters/CronConverter.cs +++ b/API/Helpers/Converters/CronConverter.cs @@ -12,18 +12,21 @@ public static class CronConverter "daily", "weekly", }; - public static string ConvertToCronNotation(string source) + /// + /// Converts to Cron Notation + /// + /// Defaults to daily + /// + public static string ConvertToCronNotation(string? source) { - var destination = string.Empty; - destination = source.ToLower() switch + if (string.IsNullOrEmpty(source)) return Cron.Daily(); + return source.ToLower() switch { "daily" => Cron.Daily(), "weekly" => Cron.Weekly(), "disabled" => Cron.Never(), "" => Cron.Never(), - _ => destination + _ => source }; - - return destination; } } diff --git a/API/Helpers/Converters/FilterFieldValueConverter.cs b/API/Helpers/Converters/FilterFieldValueConverter.cs index dd5630aeb..09e1421ab 100644 --- a/API/Helpers/Converters/FilterFieldValueConverter.cs +++ b/API/Helpers/Converters/FilterFieldValueConverter.cs @@ -77,6 +77,7 @@ public static class FilterFieldValueConverter .Select(x => (MangaFormat) Enum.Parse(typeof(MangaFormat), x)) .ToList(), FilterField.ReadTime => int.Parse(value), + FilterField.AverageRating => float.Parse(value), _ => throw new ArgumentException("Invalid field type") }; } diff --git a/API/Helpers/Converters/ServerSettingConverter.cs b/API/Helpers/Converters/ServerSettingConverter.cs index ffae4d5a8..c356bb907 100644 --- a/API/Helpers/Converters/ServerSettingConverter.cs +++ b/API/Helpers/Converters/ServerSettingConverter.cs @@ -23,12 +23,15 @@ public class ServerSettingConverter : ITypeConverter, case ServerSettingKey.TaskScan: destination.TaskScan = row.Value; break; - case ServerSettingKey.LoggingLevel: - destination.LoggingLevel = row.Value; - break; case ServerSettingKey.TaskBackup: destination.TaskBackup = row.Value; break; + case ServerSettingKey.TaskCleanup: + destination.TaskCleanup = row.Value; + break; + case ServerSettingKey.LoggingLevel: + destination.LoggingLevel = row.Value; + break; case ServerSettingKey.Port: destination.Port = int.Parse(row.Value); break; @@ -47,9 +50,6 @@ public class ServerSettingConverter : ITypeConverter, case ServerSettingKey.BookmarkDirectory: destination.BookmarksDirectory = row.Value; break; - case ServerSettingKey.EmailServiceUrl: - destination.EmailServiceUrl = row.Value; - break; case ServerSettingKey.InstallVersion: destination.InstallVersion = row.Value; break; @@ -83,6 +83,45 @@ public class ServerSettingConverter : ITypeConverter, case ServerSettingKey.CoverImageSize: destination.CoverImageSize = Enum.Parse(row.Value); break; + case ServerSettingKey.BackupDirectory: + destination.BookmarksDirectory = row.Value; + break; + case ServerSettingKey.EmailHost: + destination.SmtpConfig ??= new SmtpConfigDto(); + destination.SmtpConfig.Host = row.Value; + break; + case ServerSettingKey.EmailPort: + destination.SmtpConfig ??= new SmtpConfigDto(); + destination.SmtpConfig.Port = string.IsNullOrEmpty(row.Value) ? 0 : int.Parse(row.Value); + break; + case ServerSettingKey.EmailAuthPassword: + destination.SmtpConfig ??= new SmtpConfigDto(); + destination.SmtpConfig.Password = row.Value; + break; + case ServerSettingKey.EmailAuthUserName: + destination.SmtpConfig ??= new SmtpConfigDto(); + destination.SmtpConfig.UserName = row.Value; + break; + case ServerSettingKey.EmailSenderAddress: + destination.SmtpConfig ??= new SmtpConfigDto(); + destination.SmtpConfig.SenderAddress = row.Value; + break; + case ServerSettingKey.EmailSenderDisplayName: + destination.SmtpConfig ??= new SmtpConfigDto(); + destination.SmtpConfig.SenderDisplayName = row.Value; + break; + case ServerSettingKey.EmailEnableSsl: + destination.SmtpConfig ??= new SmtpConfigDto(); + destination.SmtpConfig.EnableSsl = bool.Parse(row.Value); + break; + case ServerSettingKey.EmailSizeLimit: + destination.SmtpConfig ??= new SmtpConfigDto(); + destination.SmtpConfig.SizeLimit = int.Parse(row.Value); + break; + case ServerSettingKey.EmailCustomizedTemplates: + destination.SmtpConfig ??= new SmtpConfigDto(); + destination.SmtpConfig.CustomizedTemplates = bool.Parse(row.Value); + break; } } diff --git a/API/Helpers/LibraryTypeHelper.cs b/API/Helpers/LibraryTypeHelper.cs index 53f5e5b60..b65c31512 100644 --- a/API/Helpers/LibraryTypeHelper.cs +++ b/API/Helpers/LibraryTypeHelper.cs @@ -13,8 +13,7 @@ public static class LibraryTypeHelper { LibraryType.Manga => MediaFormat.Manga, LibraryType.Comic => MediaFormat.Comic, - LibraryType.Book => MediaFormat.LightNovel, - _ => throw new ArgumentOutOfRangeException(nameof(libraryType), libraryType, null) + LibraryType.LightNovel => MediaFormat.LightNovel, }; } } diff --git a/API/Helpers/RateLimiter.cs b/API/Helpers/RateLimiter.cs new file mode 100644 index 000000000..c89fc2778 --- /dev/null +++ b/API/Helpers/RateLimiter.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; + +namespace API.Helpers; + +public class RateLimiter(int maxRequests, TimeSpan duration, bool refillBetween = true) +{ + private readonly Dictionary _tokenBuckets = new(); + private readonly object _lock = new(); + + public bool TryAcquire(string key) + { + lock (_lock) + { + if (!_tokenBuckets.TryGetValue(key, out var bucket)) + { + bucket = (Tokens: maxRequests, LastRefill: DateTime.UtcNow); + _tokenBuckets[key] = bucket; + } + + RefillTokens(key); + + lock (_lock) + { + + if (_tokenBuckets[key].Tokens > 0) + { + _tokenBuckets[key] = (Tokens: _tokenBuckets[key].Tokens - 1, LastRefill: _tokenBuckets[key].LastRefill); + return true; + } + } + + return false; + } + } + + private void RefillTokens(string key) + { + lock (_lock) + { + var now = DateTime.UtcNow; + var timeSinceLastRefill = now - _tokenBuckets[key].LastRefill; + var tokensToAdd = (int) (timeSinceLastRefill.TotalSeconds / duration.TotalSeconds); + + // Refill the bucket if the elapsed time is greater than or equal to the duration + if (timeSinceLastRefill >= duration) + { + _tokenBuckets[key] = (Tokens: maxRequests, LastRefill: now); + Console.WriteLine($"Tokens Refilled to Max: {maxRequests}"); + } + else if (tokensToAdd > 0 && refillBetween) + { + _tokenBuckets[key] = (Tokens: Math.Min(maxRequests, _tokenBuckets[key].Tokens + tokensToAdd), LastRefill: now); + Console.WriteLine($"Tokens Refilled: {_tokenBuckets[key].Tokens}"); + } + } + } +} + diff --git a/API/I18N/ar.json b/API/I18N/ar.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/API/I18N/ar.json @@ -0,0 +1 @@ +{} diff --git a/API/I18N/cs.json b/API/I18N/cs.json index 79b614c25..043994c23 100644 --- a/API/I18N/cs.json +++ b/API/I18N/cs.json @@ -159,5 +159,23 @@ "send-to-permission": "Nelze odeslat non-EPUB nebo PDF do zařízení, která nejsou podporována na Kindle", "reading-list-title-required": "Název seznamu čtení nemůže být prázdný", "series-restricted-age-restriction": "Uživatel nemá povoleno sledovat tuto sérii z důvodu věkového omezení", - "collection-deleted": "Sbírka smazána" + "collection-deleted": "Sbírka smazána", + "smart-filter-already-in-use": "S tímto chytrým filtrem již existuje stream", + "smart-filters": "Chytré filtry", + "browse-smart-filters": "Prohlížet podle chytrých filtrů", + "smart-filter-doesnt-exist": "Chytrý filtr neexistuje", + "sidenav-stream-doesnt-exist": "Stránková navigace streamu neexistuje", + "external-source-already-exists": "Externí zdroj již existuje", + "external-source-required": "Vyžaduje se ApiKey a Host", + "more-in-genre": "Více v žánru {0}", + "browse-recently-updated": "Procházet naposledy aktualizované", + "external-sources": "Externí zdroje", + "browse-external-sources": "Procházet externí zdroje", + "dashboard-stream-doesnt-exist": "Přehledová deska streamu neexistuje", + "external-source-doesnt-exist": "Externí zdroj neexistuje", + "external-source-already-in-use": "To je existující stream s tímto externím zdrojem", + "unable-to-reset-k+": "Licenci Kavita+ nelze resetovat kvůli chybě. Obraťte se na podporu Kavita+", + "browse-more-in-genre": "Procházet další v {0}", + "recently-updated": "Nedávno aktualizované", + "invalid-email": "E-mail v záznamech pro uživatele není platný e-mail. Všechny odkazy najdete v protokolech." } diff --git a/API/I18N/de.json b/API/I18N/de.json index 1b42b2cc7..50a7557d4 100644 --- a/API/I18N/de.json +++ b/API/I18N/de.json @@ -171,5 +171,10 @@ "external-source-doesnt-exist": "Externe Quelle existiert nicht", "external-sources": "Externe Quellen", "external-source-required": "ApiSchlüssel und Host erforderlich", - "smart-filter-already-in-use": "Es gibt einen bestehenden Stream mit diesem Smart Filter" + "smart-filter-already-in-use": "Es gibt einen bestehenden Stream mit diesem Smart Filter", + "more-in-genre": "Mehr in Genre {0}", + "browse-more-in-genre": "Mehr in {0} stöbern", + "recently-updated": "Zuletzt aktualisiert", + "browse-recently-updated": "Zuletzt aktualisiert durchsuchen", + "unable-to-reset-k+": "Aufgrund eines Fehlers konnte die Kavita+ Lizenz nicht zurückgesetzt werden. Kontaktieren Sie den Kavita+ Support" } diff --git a/API/I18N/en.json b/API/I18N/en.json index b7ddc1128..f24e76d9d 100644 --- a/API/I18N/en.json +++ b/API/I18N/en.json @@ -39,6 +39,7 @@ "admin-already-exists": "Admin already exists", "invalid-username": "Invalid username", "critical-email-migration": "There was an issue during email migration. Contact support", + "email-not-enabled": "Email is not enabled on this server. You cannot perform this action.", "chapter-doesnt-exist": "Chapter does not exist", "file-missing": "File was not found in book", @@ -53,7 +54,9 @@ "generic-device-update": "There was an error when updating the device", "generic-device-delete": "There was an error when deleting the device", "greater-0": "{0} must be greater than 0", - "send-to-kavita-email": "Send to device cannot be used with Kavita's email service. Please configure your own.", + "send-to-kavita-email": "Send to device cannot be used without Email setup", + "send-to-unallowed":"You cannot send to a device that isn't yours", + "send-to-size-limit": "The file(s) you are trying to send are too large for your emailer", "send-to-device-status": "Transferring files to your device", "generic-send-to": "There was an error sending the file(s) to the device", "series-doesnt-exist": "Series does not exist", diff --git a/API/I18N/es.json b/API/I18N/es.json index 9708f4a05..bc55639ae 100644 --- a/API/I18N/es.json +++ b/API/I18N/es.json @@ -38,7 +38,7 @@ "forgot-password-generic": "Se enviará el correo si la dirección existe en nuestra base de datos", "generic-device-create": "Ha ocurrido un error al crear el dispositivo", "greater-0": "{0} debe ser mayor que 0", - "send-to-kavita-email": "Enviar al dispositivo no se puede utilizar con el servicio de correo electrónico de Kavita. Por favor, configura el tuyo propio.", + "send-to-kavita-email": "Enviar al dispositivo no se puede utilizar sin configurar el correo electrónico", "no-cover-image": "No hay imagen de portada", "bookmark-doesnt-exist": "El marcador no existe", "generic-favicon": "Ha ocurrido un error al obtener el icono para el dominio", @@ -177,5 +177,8 @@ "more-in-genre": "Más en el género {0}", "recently-updated": "Actualizado recientemente", "browse-recently-updated": "Examinar las últimas actualizaciones", - "unable-to-reset-k+": "No se ha podido restablecer la licencia de Kavita+ debido a un error. Contacta con el soporte de Kavita" + "unable-to-reset-k+": "No se ha podido restablecer la licencia de Kavita+ debido a un error. Contacta con el soporte de Kavita", + "send-to-unallowed": "No puedes enviar a un dispositivo que no sea el tuyo", + "email-not-enabled": "El correo electrónico no está habilitado en este servidor. No puede realizar esta acción.", + "send-to-size-limit": "El(Los) archivo(s) que intenta enviar es(son) demasiado(s) grande(s) para su programa de correo electrónico" } diff --git a/API/I18N/fr.json b/API/I18N/fr.json index f39c2fa96..b106d19d7 100644 --- a/API/I18N/fr.json +++ b/API/I18N/fr.json @@ -41,7 +41,7 @@ "chapter-doesnt-exist": "Chapitre non existant", "file-missing": "Fichier introuvable dans le livre", "generic-device-delete": "Erreur lors de la suppression de l'appareil", - "send-to-kavita-email": "Envoyer à l'appareil ne peut pas être utilisé par le service e-mail de Kavita. Veuillez configurer le votre.", + "send-to-kavita-email": "Envoyer à l'appareil ne peut pas être utilisé sans configurer E-mail", "generic-favicon": "Erreur lors de la récupération de la favicon pour le domaine", "generic-library": "Erreur critique. Essayez à nouveau.", "delete-library-while-scan": "Vous ne pouvez pas supprimer une bibliothèque lorsqu'une analyse est en cours. Veuillez attendre la fin de l'analyse ou redémarrez Kavita, puis essayez de la supprimer", @@ -177,5 +177,8 @@ "more-in-genre": "Plus dans le genre {0}", "recently-updated": "Récemment mis à jour", "browse-recently-updated": "Parcourir les mises à jour récentes", - "unable-to-reset-k+": "Impossible de réinitialiser la licence Kavita+ en raison d'une erreur. Contactez le support Kavita+" + "unable-to-reset-k+": "Impossible de réinitialiser la licence Kavita+ en raison d'une erreur. Contactez le support Kavita+", + "email-not-enabled": "E-mail non activé sur ce serveur. Vous ne pouvez pas lancer cette action.", + "send-to-unallowed": "Vous ne pouvez envoyer à un appareil qui ne vous appartient pas", + "send-to-size-limit": "Le(s) fichier(s) que vous essayez d'envoyer sont trop lourds pour votre emailer" } diff --git a/API/I18N/id.json b/API/I18N/id.json index c58945b13..cd4fe1abb 100644 --- a/API/I18N/id.json +++ b/API/I18N/id.json @@ -26,5 +26,75 @@ "forgot-password-generic": "Email akan dikirimkan apabila ditemukan di database kami", "no-user": "Pengguna tidak ditemukan", "generic-invite-user": "Terdapat masalah saat mengundang pengguna ini. Tolong lihat catatan.", - "permission-denied": "Kamu tidak diizinkan untuk melakukan ini" + "permission-denied": "Kamu tidak diizinkan untuk melakukan ini", + "locked-out": "Akses anda dikunci sementara dikarenakan terlalu banyak melakukan otoriasi. Mohon tunggu 10 menit.", + "confirm-token-gen": "Terjadi kesalahan saat membuat token konfirmasi", + "unable-to-reset-key": "Terjadi kesalahan, tidak dapat menyetel ulang kunci", + "invalid-payload": "Muatan tidak valid", + "nothing-to-do": "Tidak ada yang perlu dilakukan", + "share-multiple-emails": "Anda tidak dapat berbagi email ke banyak akun", + "generic-user-update": "Terjadi kesalahan saat memperbaharui pengguna", + "manual-setup-fail": "Penyiapan manual tidak dapat diselesaikan. Harap batalkan dan buat ulang undangannya", + "generic-password-update": "Terjadi kesalahan saat mengkonfirmasi password baru", + "collection-updated": "Kumpulan telah diubah", + "collection-deleted": "Kumpulan telah dihapus", + "device-doesnt-exist": "Perangkat tidak tersedia", + "generic-error": "Terjadi kesalahan, coba lagi", + "collection-doesnt-exist": "Kumpulan tidak tersedia", + "pdf-doesnt-exist": "PDF tidak ada padahal seharusnya ada", + "bookmark-permission": "Anda tidak memiliki izin untuk menandai/membatalkan penanda", + "generic-clear-bookmarks": "Tidak dapat menghapus penanda buku", + "generic-read-progress": "Terjadi masalah saat menyimpan progres", + "reading-list-updated": "Telah diubah", + "perform-scan": "Mohon lakukan pemindaian pada seri atau pustaka ini dan coba lagi", + "series-restricted": "Pengguna tidak memiliki akses ke Seri ini", + "generic-scrobble-hold": "Terjadi kesalahan saat menambahkan pemesanan", + "job-already-running": "Pekerjaan sudah berjalan", + "generic-relationship": "Ada masalah dalam memperbarui hubungan", + "ip-address-invalid": "Alamat IP '{0}' tidak valid", + "bookmark-dir-permissions": "Izin Direktori Penanda Buku tidak benar untuk digunakan oleh Kavita", + "stats-permission-denied": "Anda tidak diizinkan untuk melihat statistik pengguna lain", + "url-required": "Untuk menggunakan, Anda harus menyertakan URL", + "generic-cover-series-save": "Tidak dapat menyimpan gambar sampul ke Seri", + "url-not-valid": "URL tidak mengembalikan gambar yang valid atau memerlukan otorisasi", + "generic-cover-library-save": "Tidak dapat menyimpan gambar sampul ke Pustaka", + "generic-cover-chapter-save": "Tidak dapat menyimpan gambar sampul ke Bab", + "reset-chapter-lock": "Tidak dapat mereset kunci sampul untuk Bab", + "bad-credentials": "Kredensial anda salah", + "no-image-for-page": "Gambar untuk halaman {0} tidak ditemukan. Coba perbarui halaman untuk memperbaharui cache.", + "encode-as-warning": "Anda tidak dapat mengonversi ke PNG. Untuk sampul, gunakan Perbarui Sampul. Penanda dan favicon tidak dapat diubah kembali.", + "total-logs": "Total log harus diantara 1 dan 30", + "total-backups": "Total backup harus diantara 1 dan 30", + "user-migration-needed": "Pengguna ini perlu migrasi. Minta pengguna tersebut log out dan login kembali untuk memicu alur migrasi", + "generic-invite-email": "Terdapat masalah saat mengirim ulang undangan email", + "admin-already-exists": "Admin sudah ada", + "invalid-username": "Nama pengguna salah", + "critical-email-migration": "Terjadi kesalahan saat migrasi email. Hubungi bantuan", + "chapter-doesnt-exist": "Bab tidak ada", + "file-missing": "Berkas tidak ditemukan didalam buku", + "invalid-access": "Akses tidak valid", + "bookmark-save": "Tidak dapat menyimpan penanda buku", + "cache-file-find": "Tidak dapat menemukan gambar yang telah disimpan dalam penyimpanan sementara. Muat ulang dan coba lagi.", + "name-required": "Nama tidak boleh kosong", + "valid-number": "Nomor halaman harus valid", + "duplicate-bookmark": "Entri penanda buku yang sama sudah ada", + "reading-list-permission": "Anda tidak memiliki izin pada daftar bacaan ini atau daftar tersebut tidak ada", + "reading-list-position": "Tidak dapat memperbarui posisi", + "reading-list-item-delete": "Tidak dapat menghapus item", + "reading-list-deleted": "Daftar Bacaan telah dihapus", + "generic-reading-list-delete": "Terjadi kesalahan saat menghapus daftar bacaan", + "generic-reading-list-update": "Terjadi kesalahan saat mengubah daftar bacaan", + "generic-reading-list-create": "Terjadi kesalahan saat membuat daftar bacaan", + "reading-list-doesnt-exist": "Daftar bacaan tidak ada", + "libraries-restricted": "Pengguna tidak memiliki akses ke pustaka manapun", + "no-series": "Gagal mengambil seri untuk pustaka ini", + "no-series-collection": "Gagal mengambil seri untuk Koleksi", + "generic-series-delete": "Terjadi kesalahan saat menghapus seri", + "generic-series-update": "Terjadi kesalahan saat mengubah seri", + "series-updated": "Berhasil diubah", + "update-metadata-fail": "Tidak dapat mengubah metadata", + "age-restriction-not-applicable": "Tidak ada batasan", + "generic-cover-collection-save": "Tidak dapat menyimpan gambar sampul ke Koleksi", + "generic-cover-reading-list-save": "Tidak dapat menyimpan gambar sampul ke Daftar Baca", + "generic-user-delete": "Tidak dapat menghapus pengguna" } diff --git a/API/I18N/ja.json b/API/I18N/ja.json index e3c0d48b4..8fd45630c 100644 --- a/API/I18N/ja.json +++ b/API/I18N/ja.json @@ -51,7 +51,134 @@ "generic-device-update": "デバイスを更新する際にエラーがありました", "generic-device-delete": "デバイスを削除した際にエラーがありました", "greater-0": "{0} は 0 よりも大きいです", - "send-to-kavita-email": "Kavitaのメールサービスでは、デバイスへの送信はできません。 自分で設定してください.", + "send-to-kavita-email": "メールの設定がないと、デバイスへの送信は使用できません", "send-to-device-status": "ファイルをデバイスに転送する", - "generic-send-to": "ファイルをデバイスに送信する際にエラーが発生しました。" + "generic-send-to": "ファイルをデバイスに送信する際にエラーが発生しました", + "email-not-enabled": "メールがこのサーバーでは有効になっていません。このアクションは実行することができません.", + "send-to-unallowed": "あなたものでないデバイスには送信できません", + "bookmarks-empty": "ブックマークを空にすることはできません", + "delete-library-while-scan": "スキャンが進行中の間はライブラリを削除することはできません。スキャンが完了するのを待つか、Kavitaを再起動してから削除を試みてください.", + "perform-scan": "このシリーズもしくはライブラリのスキャンを実行して再試行してください", + "generic-read-progress": "進捗の保存中に問題が発生しました", + "cache-file-find": "キャッシュイメージが見つかりませんでした。リロードしてもう一度試してください.", + "name-required": "空白の名前はつけることができません", + "reading-list-item-delete": "アイテムを削除できませんでした", + "reading-list-deleted": "リーディングリストは削除されました", + "generic-reading-list-delete": "リーディングリストを削除している際に問題が発生しました。", + "generic-reading-list-update": "リーディングリストを更新している際に問題が発生しました", + "no-series-collection": "コレクションのシリーズを取得できませんでした", + "generic-series-delete": "シリーズの削除中に問題が発生しました", + "generic-series-update": "シリーズの更新中にエラーが発生しました。", + "series-updated": "アップロードが成功しました", + "total-backups": "総バックアップ数は1から30の間でなければなりません", + "url-required": "使用するにはURLを渡す必要があります", + "generic-cover-series-save": "シリーズにカバー画像を保存できません", + "user-doesnt-exist": "ユーザーが存在しません", + "generic-library": "重大な問題が発生しました。もう一度試してください.", + "reading-list-permission": "リーディングリストを閲覧する権限がないかリストが存在しません", + "reading-list-position": "位置が更新できませんでした", + "reading-list-updated": "アップデート済", + "reading-list-doesnt-exist": "リーディングリストが存在しません", + "generic-scrobble-hold": "保留の追加時にエラーが発生しました。", + "no-series": "ライブラリのシリーズを取得できませんでした。", + "series-restricted": "このシリーズに対するアクセス権がありません", + "generic-relationship": "関係の更新中に問題が発生しました", + "ip-address-invalid": "IPアドレス '{0}' は無効です", + "bookmark-dir-permissions": "ブックマークディレクトリには、Kavitaが使用するための正しい権限がありません", + "url-not-valid": "URLは有効な画像を返さないか、認証が必要です", + "volume-doesnt-exist": "巻が存在しません", + "library-name-exists": "そのライブラリ名はすでに存在します。一意な名前をサーバーで選択してください.", + "invalid-path": "無効なパス", + "generic-library-update": "ライブラリを更新中に重大な問題が発生しました.", + "invalid-access": "アクセスが無効です", + "no-image-for-page": "ページ{0}の画像が存在しません。再キャッシュを許可するためにリフレッシュを試してみてください。", + "generic-clear-bookmarks": "ブックマークをクリアできませんでした", + "bookmark-permission": "ブックマーク/ブックマークを解除する権限があなたにはありません。", + "valid-number": "有効なページ番号である必要があります", + "duplicate-bookmark": "重複したブックマークエントリーがすでに存在します", + "send-to-size-limit": "送信しようとしているファイルはメーラーにとっては大きすぎます。", + "series-doesnt-exist": "シリーズが存在しません", + "pdf-doesnt-exist": "PDFが存在すべきですが存在しません。", + "generic-reading-list-create": "リーディングリストを作成している際に問題が発生しました", + "libraries-restricted": "ライブラリに対してのアクセス権がありません", + "job-already-running": "ジョブはすでに実行されています。", + "encode-as-warning": "PNGに変換することはできません。カバーの場合は「Refresh Covers」を使用してください。ブックマークとファビコンは元に戻すことはできません。", + "total-logs": "総ログ数は1から30の間でなければなりません", + "stats-permission-denied": "他のユーザーの統計を表示する権限がありません", + "bookmark-doesnt-exist": "ブックマークが存在しません", + "must-be-defined": "{0} を定義する必要があります。", + "generic-favicon": "ドメインのファビコンを取得する際に問題が発生しました。", + "invalid-filename": "無効なファイル名", + "file-doesnt-exist": "ファイルが存在しません", + "update-metadata-fail": "メタデータがアップロードできませんでした。", + "age-restriction-not-applicable": "無制限", + "no-cover-image": "カバー画像がありません", + "no-library-access": "ユーザーはこのライブラリのアクセス権がありません。", + "library-doesnt-exist": "ライブラリが存在しません。", + "bookmark-save": "ブックマークを保存できませんでした", + "smart-filter-already-in-use": "このスマートフィルターに対応する既存のストリームがあります", + "generic-cover-collection-save": "コレクションへのカバー画像の保存ができません", + "generic-cover-reading-list-save": "読書リストにカバー画像を保存できません", + "generic-cover-chapter-save": "チャプターへのカバー画像の保存ができません。", + "collections": "すべてのコレクション", + "browse-recently-added": "最近追加されたものを表示", + "browse-collections": "コレクションによるブラウズ", + "search-description": "シリーズ、コレクション、または読書リストを検索します", + "external-source-already-exists": "外部ソースは既に存在する", + "external-source-required": "ApiKeyとホストが必要", + "external-source-doesnt-exist": "外部ソースが存在しません", + "external-source-already-in-use": "この外部ソースで既存のストリームがあります", + "epub-malformed": "ファイルが不正です。読み取ることができません。", + "epub-html-missing": "そのページに適したHTMLが見つかりませんでした", + "device-duplicate": "このデバイス名はすでに存在しています", + "reading-list-name-exists": "この名前の読書リストはすでに存在しています", + "browse-more-in-genre": "もっと見る {0}", + "recently-updated": "最近更新", + "browse-recently-updated": "最近のアップロードを見る", + "smart-filters": "スマートフィルター", + "browse-external-sources": "外部ソースを参照", + "browse-smart-filters": "スマートフィルタでブラウズ", + "reading-list-restricted": "読書リストは存在しませんか、アクセスがない", + "search": "検索", + "dashboard-stream-doesnt-exist": "Dashboard Stream は存在しません", + "sidenav-stream-doesnt-exist": "SideNav Stream は存在しません", + "reading-list-title-required": "読書リスト タイトルは空にすることはできません", + "progress-must-exist": "進めるにはユーザーが存在する必要があります", + "send-to-permission": "Kindleでは、EPUBやPDF以外の形式はサポートされていないため、デバイスに送信できません", + "volume-num": "ボリューム {0}", + "book-num": "本 {0}", + "issue-num": "問題 {0}{1}", + "reset-chapter-lock": "チャプター用のカバーロックをリセットできない", + "on-deck": "デッキ", + "browse-on-deck": "デッキで見る", + "reading-lists": "リーディングリスト", + "browse-reading-lists": "読書リストで閲覧", + "more-in-genre": "さらなるジャンル {0}", + "external-sources": "外部ソース", + "smart-filter-doesnt-exist": "スマートフィルタは存在しません", + "theme-doesnt-exist": "テーマファイル欠落または無効", + "bad-copy-files-for-download": "ファイルをtempディレクトリのアーカイブのダウンロードにコピーできません.", + "unable-to-reset-k+": "エラーによるKavita +ライセンスをリセットできません。 Kavita+サポートへのアクセス", + "collection-tag-title-required": "コレクションタイトルは空にすることはできません", + "device-not-created": "このデバイスはまだ存在しません。 まずは作成しましょう", + "want-to-read": "読みたい", + "browse-want-to-read": "読みたいを表示", + "query-required": "クエリパラメータを渡す必要があります", + "not-authenticated": "ユーザが認証されていない", + "anilist-cred-expired": "AniList 認証が期限切れまたは設定されていない", + "favicon-doesnt-exist": "ファビコンは存在しません", + "unable-to-register-k+": "エラーによりライセンスを登録できません。 Kavita+サポートへのアクセス", + "series-restricted-age-restriction": "年齢制限により、ユーザーはこのシリーズを表示することが許可されていません", + "generic-cover-library-save": "ライブラリへのカバー画像の保存ができません。", + "access-denied": "アクセスがない", + "generic-user-delete": "ユーザを削除できません", + "generic-user-pref": "問題の保存設定があった", + "opds-disabled": "このサーバではOPDSが有効になっていない", + "recently-added": "最近追加", + "libraries": "すべてのライブラリ", + "browse-libraries": "ライブラリでブラウズする", + "scrobble-bad-payload": "Scrobbleプロバイダからの悪いペイロード", + "generic-create-temp-archive": "一時アーカイブの作成中に問題が発生しました", + "user-no-access-library-from-series": "ユーザーは、このシリーズが所属するライブラリにアクセス権限がありません", + "collection-tag-duplicate": "この名前のコレクションは既に存在しています" } diff --git a/API/I18N/ko.json b/API/I18N/ko.json index 898e12d1c..28ca19827 100644 --- a/API/I18N/ko.json +++ b/API/I18N/ko.json @@ -69,7 +69,7 @@ "generic-device-create": "장치를 생성하는 중에 오류가 발생했습니다", "device-doesnt-exist": "장치가 존재하지 않습니다", "generic-device-update": "장치를 업데이트 하는 중에 오류가 발생했습니다", - "send-to-kavita-email": "장치로 보내기는 Kavita의 이메일 서비스와 함께 사용할 수 없습니다. 직접 구성하십시오.", + "send-to-kavita-email": "이메일 설정 없이는 기기로 전송할 수 없습니다.", "no-cover-image": "표지 이미지 없음", "series-doesnt-exist": "시리즈가 존재하지 않습니다", "bookmarks-empty": "북마크는 비워둘 수 없습니다", @@ -176,5 +176,9 @@ "browse-more-in-genre": "{0}에서 더 찾아보기", "more-in-genre": "장르 {0}에서 더 보기", "recently-updated": "최근 업데이트됨", - "browse-recently-updated": "최근에 업데이트된 내용을 찾아보기" + "browse-recently-updated": "최근에 업데이트된 내용을 찾아보기", + "email-not-enabled": "이 서버에서는 이메일이 활성화되어 있지 않습니다. 이 작업을 수행할 수 없습니다.", + "send-to-size-limit": "보내려고 하는 파일은 이메일 전송 용량을 초과했습니다", + "send-to-unallowed": "본인 이외의 기기로는 전송할 수 없습니다", + "unable-to-reset-k+": "오류로 인해 Kavita+ 라이선스를 재설정할 수 없습니다. Kavita+ 지원팀에 문의하십시오" } diff --git a/API/I18N/nl.json b/API/I18N/nl.json index 29f71cf95..7d9d2a348 100644 --- a/API/I18N/nl.json +++ b/API/I18N/nl.json @@ -156,5 +156,8 @@ "chapter-num": "Hoofdstuk {0}", "generic-scrobble-hold": "Er is een fout opgetreden bij het toevoegen van de bewaarplicht", "on-deck": "Aan het lezen", - "browse-on-deck": "Aan Het Lezen doorbladeren" + "browse-on-deck": "Aan Het Lezen doorbladeren", + "email-not-enabled": "Email is niet aangezet op deze server. Je kan deze actie niet uitvoeren.", + "collection-deleted": "Collectie verwijderd", + "invalid-email": "De email op het bestand van de gebruiker is geen geldige email. Bekijk het lognoek voor mogelijke weblinks." } diff --git a/API/I18N/pt.json b/API/I18N/pt.json index 28ea07cc1..b414f7ab2 100644 --- a/API/I18N/pt.json +++ b/API/I18N/pt.json @@ -20,7 +20,7 @@ "generic-device-create": "Ocorreu um erro ao criar o dispositivo", "generic-device-update": "Ocorreu um erro ao atualizar o dispositivo", "greater-0": "{0} tem de ser superior a 0", - "send-to-kavita-email": "Enviar para dispositivo não pode ser usado com o serviço de email do Kavita. Por favor configure o seu serviço de email.", + "send-to-kavita-email": "Enviar para dispositivo não pode ser usado sem a configuração de Email", "bookmark-doesnt-exist": "Marcador inexistente", "file-doesnt-exist": "Ficheiro inexistente", "delete-library-while-scan": "Não pode eliminar a biblioteca enquanto uma análise está em curso. Por favor aguarde que a análise termine ou reinicie o Kavita e depois tente eliminar novamente", @@ -177,5 +177,8 @@ "more-in-genre": "Mais do Género {0}", "recently-updated": "Recém-Atualizados", "browse-recently-updated": "Ver Recém-Atualizados", - "unable-to-reset-k+": "Não foi possível redefinir a licença do Kavita+ devido a um erro. Entre em contacto com o suporte Kavita +" + "unable-to-reset-k+": "Não foi possível redefinir a licença do Kavita+ devido a um erro. Entre em contacto com o suporte Kavita +", + "email-not-enabled": "O email não está habilitado neste servidor. Não pode executar esta ação.", + "send-to-unallowed": "Não pode enviar para um dispositivo que não é seu", + "send-to-size-limit": "Os ficheiros que está a tentar enviar são demasiado grandes para o seu remetente" } diff --git a/API/I18N/pt_BR.json b/API/I18N/pt_BR.json index 4ef45333a..7550dc9f8 100644 --- a/API/I18N/pt_BR.json +++ b/API/I18N/pt_BR.json @@ -1,7 +1,7 @@ { "generic-error": "Alguma coisa deu errado. Por favor, tente outra vez", "collection-doesnt-exist": "A coleção não existe", - "send-to-kavita-email": "Enviar para o dispositivo não pode ser usado com o serviço de e-mail da Kavita. Por favor, configure o seu próprio.", + "send-to-kavita-email": "Enviar para dispositivo não pode ser usado sem configuração de e-mail", "volume-doesnt-exist": "O volume não existe", "no-cover-image": "Sem imagem de capa", "invalid-filename": "Nome de arquivo inválido", @@ -177,5 +177,8 @@ "more-in-genre": "Mais em Gênero {0}", "recently-updated": "Atualizado Recentemente", "browse-recently-updated": "Navegar Atualizado Recentemente", - "unable-to-reset-k+": "Não foi possível redefinir a licença Kavita+ devido a um erro. Entre em contato com o suporte Kavita +" + "unable-to-reset-k+": "Não foi possível redefinir a licença Kavita+ devido a um erro. Entre em contato com o suporte Kavita +", + "send-to-unallowed": "Você não pode enviar para um dispositivo que não seja seu", + "email-not-enabled": "O e-mail não está ativado neste servidor. Você não pode executar esta ação.", + "send-to-size-limit": "Os arquivos que você está tentando enviar são muito grandes para o seu e-mail" } diff --git a/API/I18N/zh_Hans.json b/API/I18N/zh_Hans.json index e4044fcdf..2210b40a6 100644 --- a/API/I18N/zh_Hans.json +++ b/API/I18N/zh_Hans.json @@ -126,7 +126,7 @@ "user-already-registered": "用户已经注册为{0}", "critical-email-migration": "更改电子邮件地址时出现问题,请联系支持人员", "chapter-doesnt-exist": "章节不存在", - "send-to-kavita-email": "无法使用Kavita服务向设备发送电子邮件,请自行配置。", + "send-to-kavita-email": "如果没有电子邮件设置,则无法使用“发送到设备”", "generic-favicon": "获取图标时出现问题", "pdf-doesnt-exist": "PDF文件应该存在,但未找到", "no-series": "无法获取资料库中的系列", @@ -177,5 +177,8 @@ "more-in-genre": "更多类型 {0}", "recently-updated": "最近更新", "browse-recently-updated": "浏览最近更新", - "unable-to-reset-k+": "因为一些错误导致无法重置 Kavita+ 许可证。请联系 Kavita+ 支持人员" + "unable-to-reset-k+": "因为一些错误导致无法重置 Kavita+ 许可证。请联系 Kavita+ 支持人员", + "email-not-enabled": "此服务器上未启用电子邮件。您无法执行此操作。", + "send-to-unallowed": "您无法发送到不属于您的设备", + "send-to-size-limit": "您尝试发送的文件对于您的电子邮件来说太大" } diff --git a/API/Logging/LogLevelOptions.cs b/API/Logging/LogLevelOptions.cs index 3771a28fe..958792c84 100644 --- a/API/Logging/LogLevelOptions.cs +++ b/API/Logging/LogLevelOptions.cs @@ -65,16 +65,18 @@ public static class LogLevelOptions e.Properties["SourceContext"].ToString().Replace("\"", string.Empty) == "Serilog.AspNetCore.RequestLoggingMiddleware"; - // If Minimum log level is Information, swallow all Request Logging messages - if (isRequestLoggingMiddleware && LogLevelSwitch.MinimumLevel >= LogEventLevel.Information) + // If Minimum log level is Warning, swallow all Request Logging messages + if (isRequestLoggingMiddleware && LogLevelSwitch.MinimumLevel > LogEventLevel.Information) { return false; } if (isRequestLoggingMiddleware) { - if (e.Properties.ContainsKey("Path") && e.Properties["Path"].ToString().Replace("\"", string.Empty) == "/api/health") return false; - if (e.Properties.ContainsKey("Path") && e.Properties["Path"].ToString().Replace("\"", string.Empty) == "/hubs/messages") return false; + var path = e.Properties["Path"].ToString().Replace("\"", string.Empty); + if (e.Properties.ContainsKey("Path") && path == "/api/health") return false; + if (e.Properties.ContainsKey("Path") && path == "/hubs/messages") return false; + if (e.Properties.ContainsKey("Path") && path.StartsWith("/api/image")) return false; } return true; diff --git a/API/Program.cs b/API/Program.cs index c6666075b..548e57859 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -87,6 +87,27 @@ public class Program } } + // Apply Before manual migrations that need to run before actual migrations + try + { + Task.Run(async () => + { + // Apply all migrations on startup + logger.LogInformation("Running Migrations"); + + // v0.7.14 + await MigrateWantToReadExport.Migrate(context, directoryService, logger); + + await unitOfWork.CommitAsync(); + logger.LogInformation("Running Migrations - complete"); + }).GetAwaiter() + .GetResult(); + } + catch (Exception ex) + { + logger.LogCritical(ex, "An error occurred during migration"); + } + await context.Database.MigrateAsync(); diff --git a/API/Services/AccountService.cs b/API/Services/AccountService.cs index aebc24ed7..71dc0f3b6 100644 --- a/API/Services/AccountService.cs +++ b/API/Services/AccountService.cs @@ -26,10 +26,7 @@ public interface IAccountService Task> ValidateEmail(string email); Task HasBookmarkPermission(AppUser? user); Task HasDownloadPermission(AppUser? user); - Task HasChangeRestrictionRole(AppUser? user); - Task CheckIfAccessible(HttpRequest request); - Task GenerateEmailLink(HttpRequest request, string token, string routePart, string email, bool withHost = true); - + Task CanChangeAgeRestriction(AppUser? user); } public class AccountService : IAccountService @@ -37,50 +34,13 @@ public class AccountService : IAccountService private readonly UserManager _userManager; private readonly ILogger _logger; private readonly IUnitOfWork _unitOfWork; - private readonly IHostEnvironment _environment; - private readonly IEmailService _emailService; public const string DefaultPassword = "[k.2@RZ!mxCQkJzE"; - private const string LocalHost = "localhost:4200"; - public AccountService(UserManager userManager, ILogger logger, IUnitOfWork unitOfWork, - IHostEnvironment environment, IEmailService emailService) + public AccountService(UserManager userManager, ILogger logger, IUnitOfWork unitOfWork) { _userManager = userManager; _logger = logger; _unitOfWork = unitOfWork; - _environment = environment; - _emailService = emailService; - } - - /// - /// Checks if the instance is accessible. If the host name is filled out, then it will assume it is accessible as email generation will use host name. - /// - /// - /// - public async Task CheckIfAccessible(HttpRequest request) - { - var host = _environment.IsDevelopment() ? LocalHost : request.Host.ToString(); - return !string.IsNullOrEmpty((await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).HostName) || await _emailService.CheckIfAccessible(host); - } - - public async Task GenerateEmailLink(HttpRequest request, string token, string routePart, string email, bool withHost = true) - { - var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - var host = _environment.IsDevelopment() ? LocalHost : request.Host.ToString(); - var basePart = $"{request.Scheme}://{host}{request.PathBase}/"; - if (!string.IsNullOrEmpty(serverSettings.HostName)) - { - basePart = serverSettings.HostName; - if (!serverSettings.BaseUrl.Equals(Configuration.DefaultBaseUrl)) - { - var removeCount = serverSettings.BaseUrl.EndsWith('/') ? 1 : 0; - basePart += serverSettings.BaseUrl.Substring(0, serverSettings.BaseUrl.Length - removeCount); - } - } - - if (withHost) return $"{basePart}/registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}"; - return $"registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}" - .Replace("//", "/"); } public async Task> ChangeUserPassword(AppUser user, string newPassword) @@ -168,14 +128,15 @@ public class AccountService : IAccountService } /// - /// Does the user have Change Restriction permission or admin rights + /// Does the user have Change Restriction permission or admin rights and not Read Only /// /// /// - public async Task HasChangeRestrictionRole(AppUser? user) + public async Task CanChangeAgeRestriction(AppUser? user) { if (user == null) return false; var roles = await _userManager.GetRolesAsync(user); + if (roles.Contains(PolicyConstants.ReadOnlyRole)) return false; return roles.Contains(PolicyConstants.ChangePasswordRole) || roles.Contains(PolicyConstants.AdminRole); } diff --git a/API/Services/ArchiveService.cs b/API/Services/ArchiveService.cs index 8fe6207a4..1262c2cc8 100644 --- a/API/Services/ArchiveService.cs +++ b/API/Services/ArchiveService.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; +using System.Threading.Tasks; using System.Xml.Linq; using System.Xml.Serialization; using API.Archive; @@ -30,13 +31,21 @@ public interface IArchiveService ArchiveLibrary CanOpen(string archivePath); bool ArchiveNeedsFlattening(ZipArchive archive); /// - /// Creates a zip file form the listed files and outputs to the temp folder. + /// Creates a zip file form the listed files and outputs to the temp folder. This will combine into one zip of multiple zips. /// /// List of files to be zipped up. Should be full file paths. /// Temp folder name to use for preparing the files. Will be created and deleted /// Path to the temp zip /// string CreateZipForDownload(IEnumerable files, string tempFolder); + /// + /// Creates a zip file form the listed files and outputs to the temp folder. This will extract each archive and combine them into one zip. + /// + /// List of files to be zipped up. Should be full file paths. + /// Temp folder name to use for preparing the files. Will be created and deleted + /// Path to the temp zip + /// + string CreateZipFromFoldersForDownload(IList files, string tempFolder, Func, Task> progressCallback); } /// @@ -322,6 +331,53 @@ public class ArchiveService : IArchiveService return zipPath; } + public string CreateZipFromFoldersForDownload(IList files, string tempFolder, Func, Task> progressCallback) + { + var dateString = DateTime.UtcNow.ToShortDateString().Replace("/", "_"); + + var potentialExistingFile = _directoryService.FileSystem.FileInfo.New(Path.Join(_directoryService.TempDirectory, $"kavita_{tempFolder}_{dateString}.cbz")); + if (potentialExistingFile.Exists) + { + // A previous download exists, just return it immediately + return potentialExistingFile.FullName; + } + + // Extract all the files to a temp directory and create zip on that + var tempLocation = Path.Join(_directoryService.TempDirectory, $"{tempFolder}_{dateString}"); + var totalFiles = files.Count + 1; + var count = 1f; + try + { + _directoryService.ExistOrCreate(tempLocation); + foreach (var path in files) + { + var tempPath = Path.Join(tempLocation, _directoryService.FileSystem.Path.GetFileNameWithoutExtension(_directoryService.FileSystem.FileInfo.New(path).Name)); + progressCallback(Tuple.Create(_directoryService.FileSystem.FileInfo.New(path).Name, (1.0f * totalFiles) / count)); + ExtractArchive(path, tempPath); + count++; + } + } + catch + { + throw new KavitaException("bad-copy-files-for-download"); + } + + var zipPath = Path.Join(_directoryService.TempDirectory, $"kavita_{tempFolder}_{dateString}.cbz"); + try + { + ZipFile.CreateFromDirectory(tempLocation, zipPath); + // Remove the folder as we have the zip + _directoryService.ClearAndDeleteDirectory(tempLocation); + } + catch (AggregateException ex) + { + _logger.LogError(ex, "There was an issue creating temp archive"); + throw new KavitaException("generic-create-temp-archive"); + } + + return zipPath; + } + /// /// Test if the archive path exists and an archive @@ -477,7 +533,7 @@ public class ArchiveService : IArchiveService { if (!IsValidArchive(archivePath)) return; - if (Directory.Exists(extractPath)) return; + if (_directoryService.FileSystem.Directory.Exists(extractPath)) return; if (!_directoryService.FileSystem.File.Exists(archivePath)) { diff --git a/API/Services/DeviceService.cs b/API/Services/DeviceService.cs index 02cdddd62..13c51c8d5 100644 --- a/API/Services/DeviceService.cs +++ b/API/Services/DeviceService.cs @@ -107,6 +107,10 @@ public class DeviceService : IDeviceService public async Task SendTo(IReadOnlyList chapterIds, int deviceId) { + var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + if (!settings.IsEmailSetup()) + throw new KavitaException("send-to-kavita-email"); + var device = await _unitOfWork.DeviceRepository.GetDeviceById(deviceId); if (device == null) throw new KavitaException("device-doesnt-exist"); @@ -114,10 +118,15 @@ public class DeviceService : IDeviceService if (files.Any(f => f.Format is not (MangaFormat.Epub or MangaFormat.Pdf)) && device.Platform == DevicePlatform.Kindle) throw new KavitaException("send-to-permission"); + // If the size of the files is too big + if (files.Sum(f => f.Bytes) >= settings.SmtpConfig.SizeLimit) + throw new KavitaException("send-to-size-limit"); + device.UpdateLastUsed(); _unitOfWork.DeviceRepository.Update(device); await _unitOfWork.CommitAsync(); + var success = await _emailService.SendFilesToEmail(new SendToDto() { DestinationEmail = device.EmailAddress!, diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 15afddf95..e3dede802 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -26,6 +26,8 @@ public interface IDirectoryService string SiteThemeDirectory { get; } string FaviconDirectory { get; } string LocalizationDirectory { get; } + string CustomizedTemplateDirectory { get; } + string TemplateDirectory { get; } /// /// Original BookmarkDirectory. Only used for resetting directory. Use for actual path. /// @@ -81,6 +83,8 @@ public class DirectoryService : IDirectoryService public string SiteThemeDirectory { get; } public string FaviconDirectory { get; } public string LocalizationDirectory { get; } + public string CustomizedTemplateDirectory { get; } + public string TemplateDirectory { get; } private readonly ILogger _logger; private const RegexOptions MatchOptions = RegexOptions.Compiled | RegexOptions.IgnoreCase; @@ -114,6 +118,10 @@ public class DirectoryService : IDirectoryService FaviconDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "favicons"); ExistOrCreate(FaviconDirectory); LocalizationDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "I18N"); + CustomizedTemplateDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "templates"); + ExistOrCreate(CustomizedTemplateDirectory); + TemplateDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "EmailTemplates"); + ExistOrCreate(TemplateDirectory); } /// diff --git a/API/Services/EmailService.cs b/API/Services/EmailService.cs index 3086abb25..5e54e2170 100644 --- a/API/Services/EmailService.cs +++ b/API/Services/EmailService.cs @@ -1,77 +1,107 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.IO; using System.Linq; using System.Net; using System.Threading.Tasks; +using System.Web; using API.Data; using API.DTOs.Email; -using API.Entities.Enums; -using Flurl; -using Flurl.Http; using Kavita.Common; -using Kavita.Common.EnvironmentInfo; -using Kavita.Common.Helpers; +using MailKit.Security; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using MimeKit; namespace API.Services; #nullable enable +internal class EmailOptionsDto +{ + public required IList ToEmails { get; set; } + public required string Subject { get; set; } + public required string Body { get; set; } + public required string Preheader { get; set; } + public IList>? PlaceHolders { get; set; } + /// + /// Filenames to attach + /// + public IList? Attachments { get; set; } +} + public interface IEmailService { - Task SendConfirmationEmail(ConfirmationEmailDto data); + Task SendInviteEmail(ConfirmationEmailDto data); Task CheckIfAccessible(string host); - Task SendMigrationEmail(EmailMigrationDto data); - Task SendPasswordResetEmail(PasswordResetEmailDto data); + Task SendForgotPasswordEmail(PasswordResetEmailDto dto); Task SendFilesToEmail(SendToDto data); - Task TestConnectivity(string emailUrl, string adminEmail, bool sendEmail); - Task IsDefaultEmailService(); + Task SendTestEmail(string adminEmail); Task SendEmailChangeEmail(ConfirmationEmailDto data); - Task GetVersion(string emailUrl); bool IsValidEmail(string email); + + Task GenerateEmailLink(HttpRequest request, string token, string routePart, string email, + bool withHost = true); } public class EmailService : IEmailService { private readonly ILogger _logger; private readonly IUnitOfWork _unitOfWork; - private readonly IDownloadService _downloadService; + private readonly IDirectoryService _directoryService; + private readonly IHostEnvironment _environment; - /// - /// This is used to initially set or reset the ServerSettingKey. Do not access from the code, access via UnitOfWork - /// - public const string DefaultApiUrl = "https://email.kavitareader.com"; + private const string TemplatePath = @"{0}.html"; + private const string LocalHost = "localhost:4200"; - public EmailService(ILogger logger, IUnitOfWork unitOfWork, IDownloadService downloadService) + public EmailService(ILogger logger, IUnitOfWork unitOfWork, IDirectoryService directoryService, IHostEnvironment environment) { _logger = logger; _unitOfWork = unitOfWork; - _downloadService = downloadService; - - - FlurlHttp.ConfigureClient(DefaultApiUrl, cli => - cli.Settings.HttpClientFactory = new UntrustedCertClientFactory()); + _directoryService = directoryService; + _environment = environment; } /// - /// Test if this instance is accessible outside the network + /// Test if the email settings are working. Rejects if user email isn't valid or not all data is setup in server settings. /// - /// This will do some basic filtering to auto return false if the emailUrl is a LAN ip - /// - /// Should an email be sent if connectivity is successful /// - public async Task TestConnectivity(string emailUrl, string adminEmail, bool sendEmail) + public async Task SendTestEmail(string adminEmail) { - var result = new EmailTestResultDto(); + var result = new EmailTestResultDto + { + EmailAddress = adminEmail + }; + + var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + if (!IsValidEmail(adminEmail) || !settings.IsEmailSetup()) + { + result.ErrorMessage = "You need to fill in more information in settings and ensure your account has a valid email to send a test email"; + result.Successful = false; + return result; + } + + var placeholders = new List> + { + new ("{{Host}}", settings.HostName), + }; + try { - if (IsLocalIpAddress(emailUrl)) + var emailOptions = new EmailOptionsDto() { - result.Successful = false; - result.ErrorMessage = "This is a local IP address"; - } - result.Successful = await SendEmailWithGet($"{emailUrl}/api/test?adminEmail={Url.Encode(adminEmail)}&sendEmail={sendEmail}"); + Subject = "Kavita - Email Test", + Body = UpdatePlaceHolders(await GetEmailBody("EmailTest"), placeholders), + Preheader = "Kavita - Email Test", + ToEmails = new List() + { + adminEmail + } + }; + + await SendEmail(emailOptions); + result.Successful = true; } catch (KavitaException ex) { @@ -82,229 +112,232 @@ public class EmailService : IEmailService return result; } - public async Task IsDefaultEmailService() - { - return (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl))!.Value! - .Equals(DefaultApiUrl); - } - + /// + /// Sends an email that has a link that will finalize an Email Change + /// + /// public async Task SendEmailChangeEmail(ConfirmationEmailDto data) { - var emailLink = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl))!.Value; - var success = await SendEmailWithPost(emailLink + "/api/account/email-change", data); - if (!success) + var placeholders = new List> { - _logger.LogError("There was a critical error sending Confirmation email"); - } - } + new ("{{InvitingUser}}", data.InvitingUser), + new ("{{Link}}", data.ServerConfirmationLink) + }; - public async Task GetVersion(string emailUrl) - { - try + var emailOptions = new EmailOptionsDto() { - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - var response = await $"{emailUrl}/api/about/version" - .WithHeader("Accept", "application/json") - .WithHeader("User-Agent", "Kavita") - .WithHeader("x-api-key", "MsnvA2DfQqxSK5jh") - .WithHeader("x-kavita-version", BuildInfo.Version) - .WithHeader("x-kavita-installId", settings.InstallId) - .WithHeader("Content-Type", "application/json") - .WithTimeout(TimeSpan.FromSeconds(10)) - .GetStringAsync(); - - if (!string.IsNullOrEmpty(response)) + Subject = UpdatePlaceHolders("Your email has been changed on {{InvitingUser}}'s Server", placeholders), + Body = UpdatePlaceHolders(await GetEmailBody("EmailChange"), placeholders), + Preheader = UpdatePlaceHolders("Your email has been changed on {{InvitingUser}}'s Server", placeholders), + ToEmails = new List() { - return response.Replace("\"", string.Empty); + data.EmailAddress } - } - catch (Exception) - { - return null; - } + }; - return null; + await SendEmail(emailOptions); } + /// + /// Validates the email address. Does not test it actually receives mail + /// + /// + /// public bool IsValidEmail(string email) { return new EmailAddressAttribute().IsValid(email); } - public async Task SendConfirmationEmail(ConfirmationEmailDto data) + public async Task GenerateEmailLink(HttpRequest request, string token, string routePart, string email, bool withHost = true) { - var emailLink = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value; - var success = await SendEmailWithPost(emailLink + "/api/invite/confirm", data); - if (!success) + var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var host = _environment.IsDevelopment() ? LocalHost : request.Host.ToString(); + var basePart = $"{request.Scheme}://{host}{request.PathBase}"; + if (!string.IsNullOrEmpty(serverSettings.HostName)) { - _logger.LogError("There was a critical error sending Confirmation email"); - } - } - - public async Task CheckIfAccessible(string host) - { - // This is the only exception for using the default because we need an external service to check if the server is accessible for emails - try - { - if (IsLocalIpAddress(host)) + basePart = serverSettings.HostName; + if (!serverSettings.BaseUrl.Equals(Configuration.DefaultBaseUrl)) { - _logger.LogDebug("[EmailService] Server is not accessible, using local ip"); - return false; + var removeCount = serverSettings.BaseUrl.EndsWith('/') ? 1 : 0; + basePart += serverSettings.BaseUrl[..^removeCount]; } - - var url = DefaultApiUrl + "/api/reachable?host=" + host; - _logger.LogDebug("[EmailService] Checking if this server is accessible for sending an email to: {Url}", url); - return await SendEmailWithGet(url); } - catch (Exception) + + if (withHost) return $"{basePart}/registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}"; + return $"registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}" + .Replace("//", "/"); + } + + /// + /// Sends an invite email to a user to setup their account + /// + /// + public async Task SendInviteEmail(ConfirmationEmailDto data) + { + var placeholders = new List> { - return false; - } + new ("{{InvitingUser}}", data.InvitingUser), + new ("{{Link}}", data.ServerConfirmationLink) + }; + + var emailOptions = new EmailOptionsDto() + { + Subject = UpdatePlaceHolders("You've been invited to join {{InvitingUser}}'s Server", placeholders), + Body = UpdatePlaceHolders(await GetEmailBody("EmailConfirm"), placeholders), + Preheader = UpdatePlaceHolders("You've been invited to join {{InvitingUser}}'s Server", placeholders), + ToEmails = new List() + { + data.EmailAddress + } + }; + + await SendEmail(emailOptions); } - public async Task SendMigrationEmail(EmailMigrationDto data) + public Task CheckIfAccessible(string host) { - var emailLink = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value; - return await SendEmailWithPost(emailLink + "/api/invite/email-migration", data); + return Task.FromResult(true); } - public async Task SendPasswordResetEmail(PasswordResetEmailDto data) + public async Task SendForgotPasswordEmail(PasswordResetEmailDto dto) { - var emailLink = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value; - return await SendEmailWithPost(emailLink + "/api/invite/email-password-reset", data); + var placeholders = new List> + { + new ("{{Link}}", dto.ServerConfirmationLink), + }; + + var emailOptions = new EmailOptionsDto() + { + Subject = UpdatePlaceHolders("A password reset has been requested", placeholders), + Body = UpdatePlaceHolders(await GetEmailBody("EmailPasswordReset"), placeholders), + Preheader = "A password reset has been requested", + ToEmails = new List() + { + dto.EmailAddress + } + }; + + await SendEmail(emailOptions); + return true; } public async Task SendFilesToEmail(SendToDto data) { - if (await IsDefaultEmailService()) return false; - var emailLink = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value; - return await SendEmailWithFiles(emailLink + "/api/sendto", data.FilePaths, data.DestinationEmail); + var serverSetting = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + if (!serverSetting.IsEmailSetupForSendToDevice()) return false; + + var emailOptions = new EmailOptionsDto() + { + Subject = "Send file from Kavita", + Preheader = "File(s) sent from Kavita", + ToEmails = new List() + { + data.DestinationEmail + }, + Body = await GetEmailBody("SendToDevice"), + Attachments = data.FilePaths.ToList() + }; + + await SendEmail(emailOptions); + return true; } - private async Task SendEmailWithGet(string url, int timeoutSecs = 30) + private async Task SendEmail(EmailOptionsDto userEmailOptions) { + var smtpConfig = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).SmtpConfig; + var email = new MimeMessage() + { + Subject = userEmailOptions.Subject, + }; + email.From.Add(new MailboxAddress(smtpConfig.SenderDisplayName, smtpConfig.SenderAddress)); + + // Inject the body into the base template + var fullBody = UpdatePlaceHolders(await GetEmailBody("base"), new List>() + { + new ("{{Body}}", userEmailOptions.Body), + new ("{{Preheader}}", userEmailOptions.Preheader), + }); + + var body = new BodyBuilder + { + HtmlBody = fullBody + }; + + if (userEmailOptions.Attachments != null) + { + foreach (var attachment in userEmailOptions.Attachments) + { + await body.Attachments.AddAsync(attachment); + } + } + + email.Body = body.ToMessageBody(); + + foreach (var toEmail in userEmailOptions.ToEmails) + { + email.To.Add(new MailboxAddress(toEmail, toEmail)); + } + + using var smtpClient = new MailKit.Net.Smtp.SmtpClient(); + smtpClient.Timeout = 20000; + var ssl = smtpConfig.EnableSsl ? SecureSocketOptions.Auto : SecureSocketOptions.None; + + await smtpClient.ConnectAsync(smtpConfig.Host, smtpConfig.Port, ssl); + if (!string.IsNullOrEmpty(smtpConfig.UserName) && !string.IsNullOrEmpty(smtpConfig.Password)) + { + await smtpClient.AuthenticateAsync(smtpConfig.UserName, smtpConfig.Password); + } + + ServicePointManager.SecurityProtocol = SecurityProtocolType.SystemDefault; + try { - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - var response = await (url) - .WithHeader("Accept", "application/json") - .WithHeader("User-Agent", "Kavita") - .WithHeader("x-api-key", "MsnvA2DfQqxSK5jh") - .WithHeader("x-kavita-version", BuildInfo.Version) - .WithHeader("x-kavita-installId", settings.InstallId) - .WithHeader("Content-Type", "application/json") - .WithTimeout(TimeSpan.FromSeconds(timeoutSecs)) - .GetStringAsync(); - - if (!string.IsNullOrEmpty(response) && bool.Parse(response)) - { - return true; - } + await smtpClient.SendAsync(email); } catch (Exception ex) { - throw new KavitaException(ex.Message); + _logger.LogError(ex, "There was an issue sending the email"); + throw; + } + finally + { + await smtpClient.DisconnectAsync(true); } - return false; } - - private async Task SendEmailWithPost(string url, object data, int timeoutSecs = 30) + private async Task GetTemplatePath(string templateName) { - try + if ((await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).SmtpConfig.CustomizedTemplates) { - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - var response = await (url) - .WithHeader("Accept", "application/json") - .WithHeader("User-Agent", "Kavita") - .WithHeader("x-api-key", "MsnvA2DfQqxSK5jh") - .WithHeader("x-kavita-version", BuildInfo.Version) - .WithHeader("x-kavita-installId", settings.InstallId) - .WithHeader("Content-Type", "application/json") - .WithTimeout(TimeSpan.FromSeconds(timeoutSecs)) - .PostJsonAsync(data); + var templateDirectory = Path.Join(_directoryService.CustomizedTemplateDirectory, TemplatePath); + var fullName = string.Format(templateDirectory, templateName); + if (_directoryService.FileSystem.File.Exists(fullName)) return fullName; + _logger.LogError("Customized Templates is on, but template {TemplatePath} is missing", fullName); + } - if (response.StatusCode != StatusCodes.Status200OK) + return string.Format(Path.Join(_directoryService.TemplateDirectory, TemplatePath), templateName); + } + + private async Task GetEmailBody(string templateName) + { + var templatePath = await GetTemplatePath(templateName); + + var body = await File.ReadAllTextAsync(templatePath); + return body; + } + + private static string UpdatePlaceHolders(string text, IList>? keyValuePairs) + { + if (string.IsNullOrEmpty(text) || keyValuePairs == null) return text; + + foreach (var (key, value) in keyValuePairs) + { + if (text.Contains(key)) { - var errorMessage = await response.GetStringAsync(); - throw new KavitaException(errorMessage); + text = text.Replace(key, value); } } - catch (FlurlHttpException ex) - { - _logger.LogError(ex, "There was an exception when interacting with Email Service"); - return false; - } - return true; + + return text; } - - - private async Task SendEmailWithFiles(string url, IEnumerable filePaths, string destEmail, int timeoutSecs = 300) - { - try - { - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - var response = await (url) - .WithHeader("User-Agent", "Kavita") - .WithHeader("x-api-key", "MsnvA2DfQqxSK5jh") - .WithHeader("x-kavita-version", BuildInfo.Version) - .WithHeader("x-kavita-installId", settings.InstallId) - .WithTimeout(timeoutSecs) - .AllowHttpStatus("4xx") - .PostMultipartAsync(mp => - { - mp.AddString("email", destEmail); - var index = 1; - foreach (var filepath in filePaths) - { - mp.AddFile("file" + index, filepath, _downloadService.GetContentTypeFromFile(filepath)); - index++; - } - } - ); - - if (response.StatusCode != StatusCodes.Status200OK) - { - var errorMessage = await response.GetStringAsync(); - throw new KavitaException(errorMessage); - } - } - catch (FlurlHttpException ex) - { - _logger.LogError(ex, "There was an exception when sending Email for SendTo"); - return false; - } - return true; - } - - private static bool IsLocalIpAddress(string url) - { - var host = url.Split(':')[0]; - try - { - // get host IP addresses - var hostIPs = Dns.GetHostAddresses(host); - // get local IP addresses - var localIPs = Dns.GetHostAddresses(Dns.GetHostName()); - - // test if any host IP equals to any local IP or to localhost - foreach (var hostIp in hostIPs) - { - // is localhost - if (IPAddress.IsLoopback(hostIp)) return true; - // is local address - if (localIPs.Contains(hostIp)) - { - return true; - } - } - } - catch - { - // ignored - } - - return false; - } - } diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index 3e9b02118..36ba07ddc 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -458,6 +458,7 @@ public class ImageService : IImageService for (var i = 0; i < coverImages.Count; i++) { + if (!File.Exists(coverImages[i])) continue; var tile = Image.NewFromFile(coverImages[i], access: Enums.Access.Sequential); tile = tile.ThumbnailImage(thumbnailWidth, height: thumbnailHeight); diff --git a/API/Services/Plus/ExternalMetadataService.cs b/API/Services/Plus/ExternalMetadataService.cs index cda7fe76e..e0d169f60 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/API/Services/Plus/ExternalMetadataService.cs @@ -1,13 +1,22 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; using System.Threading.Tasks; using API.Data; using API.Data.Repositories; +using API.DTOs; using API.DTOs.Recommendation; using API.DTOs.Scrobbling; +using API.DTOs.SeriesDetail; +using API.Entities; using API.Entities.Enums; -using API.Helpers.Builders; +using API.Entities.Metadata; +using API.Extensions; +using API.Helpers; +using AutoMapper; using Flurl.Http; +using Hangfire; using Kavita.Common; using Kavita.Common.EnvironmentInfo; using Kavita.Common.Helpers; @@ -29,25 +38,149 @@ internal class ExternalMetadataIdsDto public MediaFormat? PlusMediaFormat { get; set; } = MediaFormat.Unknown; } +internal class SeriesDetailPlusApiDto +{ + public IEnumerable Recommendations { get; set; } + public IEnumerable Reviews { get; set; } + public IEnumerable Ratings { get; set; } + public int? AniListId { get; set; } + public long? MalId { get; set; } +} + public interface IExternalMetadataService { Task GetExternalSeriesDetail(int? aniListId, long? malId, int? seriesId); + Task GetSeriesDetailPlus(int seriesId, LibraryType libraryType); + Task ForceKavitaPlusRefresh(int seriesId); + Task FetchExternalDataTask(); + /// + /// This is an entry point and provides a level of protection against calling upstream API. Will only allow 100 new + /// series to fetch data within a day and enqueues background jobs at certain times to fetch that data. + /// + /// + /// + /// + Task GetNewSeriesData(int seriesId, LibraryType libraryType); } public class ExternalMetadataService : IExternalMetadataService { private readonly IUnitOfWork _unitOfWork; private readonly ILogger _logger; + private readonly IMapper _mapper; + private readonly ILicenseService _licenseService; + private readonly TimeSpan _externalSeriesMetadataCache = TimeSpan.FromDays(30); + public static readonly ImmutableArray NonEligibleLibraryTypes = ImmutableArray.Create(LibraryType.Comic, LibraryType.Book); + private readonly SeriesDetailPlusDto _defaultReturn = new() + { + Recommendations = null, + Ratings = ArraySegment.Empty, + Reviews = ArraySegment.Empty + }; + // Allow 50 requests per 24 hours + private static readonly RateLimiter RateLimiter = new RateLimiter(50, TimeSpan.FromHours(12), false); - public ExternalMetadataService(IUnitOfWork unitOfWork, ILogger logger) + public ExternalMetadataService(IUnitOfWork unitOfWork, ILogger logger, IMapper mapper, ILicenseService licenseService) { _unitOfWork = unitOfWork; _logger = logger; + _mapper = mapper; + _licenseService = licenseService; + FlurlHttp.ConfigureClient(Configuration.KavitaPlusApiUrl, cli => cli.Settings.HttpClientFactory = new UntrustedCertClientFactory()); } + /// + /// Checks if the library type is allowed to interact with Kavita+ + /// + /// + /// + public static bool IsPlusEligible(LibraryType type) + { + return !NonEligibleLibraryTypes.Contains(type); + } + + /// + /// This is a task that runs on a schedule and slowly fetches data from Kavita+ to keep + /// data in the DB non-stale and fetched. + /// + /// To avoid blasting Kavita+ API, this only processes a few records. The goal is to slowly build + /// + [DisableConcurrentExecution(60 * 60 * 60)] + [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] + public async Task FetchExternalDataTask() + { + // Find all Series that are eligible and limit + var ids = await _unitOfWork.ExternalSeriesMetadataRepository.GetAllSeriesIdsWithoutMetadata(25); + if (ids.Count == 0) return; + + _logger.LogInformation("[Kavita+ Data Refresh] Started Refreshing {Count} series data from Kavita+", ids.Count); + var count = 0; + var libTypes = await _unitOfWork.LibraryRepository.GetLibraryTypesBySeriesIdsAsync(ids); + foreach (var seriesId in ids) + { + var libraryType = libTypes[seriesId]; + await GetNewSeriesData(seriesId, libraryType); + await Task.Delay(1500); + count++; + } + _logger.LogInformation("[Kavita+ Data Refresh] Finished Refreshing {Count} series data from Kavita+", count); + } + + /// + /// Removes from Blacklist and Invalidates the cache + /// + /// + /// + public async Task ForceKavitaPlusRefresh(int seriesId) + { + if (!await _licenseService.HasActiveLicense()) return; + // Remove from Blacklist if applicable + var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeBySeriesIdAsync(seriesId); + if (!IsPlusEligible(libraryType)) return; + await _unitOfWork.ExternalSeriesMetadataRepository.RemoveFromBlacklist(seriesId); + var metadata = await _unitOfWork.ExternalSeriesMetadataRepository.GetExternalSeriesMetadata(seriesId); + if (metadata == null) return; + metadata.ValidUntilUtc = DateTime.UtcNow.Subtract(_externalSeriesMetadataCache); + await _unitOfWork.CommitAsync(); + } + + /// + /// Fetches data from Kavita+ + /// + /// + /// + public async Task GetNewSeriesData(int seriesId, LibraryType libraryType) + { + if (!IsPlusEligible(libraryType)) return; + + // Generate key based on seriesId and libraryType or any unique identifier for the request + // Check if the request is allowed based on the rate limit + if (!RateLimiter.TryAcquire(string.Empty)) + { + // Request not allowed due to rate limit + _logger.LogDebug("Rate Limit hit for Kavita+ prefetch"); + return; + } + + _logger.LogDebug("Prefetching Kavita+ data for Series {SeriesId}", seriesId); + // Prefetch SeriesDetail data + await GetSeriesDetailPlus(seriesId, libraryType); + + // TODO: Fetch Series Metadata + + } + + /// + /// Retrieves Metadata about a Recommended External Series + /// + /// + /// + /// + /// + /// public async Task GetExternalSeriesDetail(int? aniListId, long? malId, int? seriesId) { if (!aniListId.HasValue && !malId.HasValue) @@ -55,11 +188,198 @@ public class ExternalMetadataService : IExternalMetadataService throw new KavitaException("Unable to find valid information from url for External Load"); } + // 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; - return await GetSeriesDetail(license, aniListId, malId, seriesId); + var details = await GetSeriesDetail(license, aniListId, malId, seriesId); + + return details; } + /// + /// Returns Series Detail data from Kavita+ - Review, Recs, Ratings + /// + /// + /// + public async Task GetSeriesDetailPlus(int seriesId, LibraryType libraryType) + { + if (!IsPlusEligible(libraryType) || !await _licenseService.HasActiveLicense()) return _defaultReturn; + + // Check blacklist (bad matches) + if (await _unitOfWork.ExternalSeriesMetadataRepository.IsBlacklistedSeries(seriesId)) return _defaultReturn; + + var needsRefresh = + await _unitOfWork.ExternalSeriesMetadataRepository.ExternalSeriesMetadataNeedsRefresh(seriesId); + + if (!needsRefresh) + { + // Convert into DTOs and return + return await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesDetailPlusDto(seriesId); + } + + try + { + var data = await _unitOfWork.SeriesRepository.GetPlusSeriesDto(seriesId); + if (data == null) return _defaultReturn; + _logger.LogDebug("Fetching Kavita+ Series Detail data for {SeriesName}", data.SeriesName); + + var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; + var result = await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-detail") + .WithHeader("Accept", "application/json") + .WithHeader("User-Agent", "Kavita") + .WithHeader("x-license-key", license) + .WithHeader("x-installId", HashUtil.ServerToken()) + .WithHeader("x-kavita-version", BuildInfo.Version) + .WithHeader("Content-Type", "application/json") + .WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs)) + .PostJsonAsync(data) + .ReceiveJson(); + + + // Clear out existing results + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); + var externalSeriesMetadata = await GetExternalSeriesMetadataForSeries(seriesId, series!); + _unitOfWork.ExternalSeriesMetadataRepository.Remove(externalSeriesMetadata.ExternalReviews); + _unitOfWork.ExternalSeriesMetadataRepository.Remove(externalSeriesMetadata.ExternalRatings); + _unitOfWork.ExternalSeriesMetadataRepository.Remove(externalSeriesMetadata.ExternalRecommendations); + + externalSeriesMetadata.ExternalReviews = result.Reviews.Select(r => + { + var review = _mapper.Map(r); + review.SeriesId = externalSeriesMetadata.SeriesId; + return review; + }).ToList(); + + externalSeriesMetadata.ExternalRatings = result.Ratings.Select(r => + { + var rating = _mapper.Map(r); + rating.SeriesId = externalSeriesMetadata.SeriesId; + return rating; + }).ToList(); + + + // Recommendations + externalSeriesMetadata.ExternalRecommendations ??= new List(); + var recs = await ProcessRecommendations(libraryType, result.Recommendations, externalSeriesMetadata); + + var extRatings = externalSeriesMetadata.ExternalRatings + .Where(r => r.AverageScore > 0) + .ToList(); + + externalSeriesMetadata.ValidUntilUtc = DateTime.UtcNow.Add(_externalSeriesMetadataCache); + externalSeriesMetadata.AverageExternalRating = extRatings.Count != 0 ? (int) extRatings + .Average(r => r.AverageScore) : 0; + + if (result.MalId.HasValue) externalSeriesMetadata.MalId = result.MalId.Value; + if (result.AniListId.HasValue) externalSeriesMetadata.AniListId = result.AniListId.Value; + await _unitOfWork.CommitAsync(); + + return new SeriesDetailPlusDto() + { + Recommendations = recs, + Ratings = result.Ratings, + Reviews = externalSeriesMetadata.ExternalReviews.Select(r => _mapper.Map(r)) + }; + } + catch (FlurlHttpException ex) + { + if (ex.StatusCode == 500) + { + return _defaultReturn; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "An error happened during the request to Kavita+ API"); + } + + // Blacklist the series as it wasn't found in Kavita+ + await _unitOfWork.ExternalSeriesMetadataRepository.CreateBlacklistedSeries(seriesId); + + return _defaultReturn; + } + + + private async Task GetExternalSeriesMetadataForSeries(int seriesId, Series series) + { + var externalSeriesMetadata = await _unitOfWork.ExternalSeriesMetadataRepository.GetExternalSeriesMetadata(seriesId); + if (externalSeriesMetadata != null) return externalSeriesMetadata; + + externalSeriesMetadata = new ExternalSeriesMetadata() + { + SeriesId = seriesId, + }; + series.ExternalSeriesMetadata = externalSeriesMetadata; + _unitOfWork.ExternalSeriesMetadataRepository.Attach(externalSeriesMetadata); + return externalSeriesMetadata; + } + + private async Task ProcessRecommendations(LibraryType libraryType, IEnumerable recs, + ExternalSeriesMetadata externalSeriesMetadata) + { + var recDto = new RecommendationDto() + { + ExternalSeries = new List(), + OwnedSeries = new List() + }; + + // NOTE: This can result in a series being recommended that shares the same name but different format + foreach (var rec in recs) + { + // Find the series based on name and type and that the user has access too + var seriesForRec = await _unitOfWork.SeriesRepository.GetSeriesDtoByNamesAndMetadataIds(rec.RecommendationNames, + libraryType, ScrobblingService.CreateUrl(ScrobblingService.AniListWeblinkWebsite, rec.AniListId), + ScrobblingService.CreateUrl(ScrobblingService.MalWeblinkWebsite, rec.MalId)); + + if (seriesForRec != null) + { + recDto.OwnedSeries.Add(seriesForRec); + externalSeriesMetadata.ExternalRecommendations.Add(new ExternalRecommendation() + { + SeriesId = seriesForRec.Id, + AniListId = rec.AniListId, + MalId = rec.MalId, + Name = seriesForRec.Name, + Url = rec.SiteUrl, + CoverUrl = rec.CoverUrl, + Summary = rec.Summary, + Provider = rec.Provider + }); + continue; + } + + // We can show this based on user permissions + if (string.IsNullOrEmpty(rec.Name) || string.IsNullOrEmpty(rec.SiteUrl) || string.IsNullOrEmpty(rec.CoverUrl)) continue; + recDto.ExternalSeries.Add(new ExternalSeriesDto() + { + Name = string.IsNullOrEmpty(rec.Name) ? rec.RecommendationNames.First() : rec.Name, + Url = rec.SiteUrl, + CoverUrl = rec.CoverUrl, + Summary = rec.Summary, + AniListId = rec.AniListId, + MalId = rec.MalId + }); + externalSeriesMetadata.ExternalRecommendations.Add(new ExternalRecommendation() + { + SeriesId = null, + AniListId = rec.AniListId, + MalId = rec.MalId, + Name = rec.Name, + Url = rec.SiteUrl, + CoverUrl = rec.CoverUrl, + Summary = rec.Summary, + Provider = rec.Provider + }); + } + + recDto.OwnedSeries = recDto.OwnedSeries.DistinctBy(s => s.Id).OrderBy(r => r.Name).ToList(); + recDto.ExternalSeries = recDto.ExternalSeries.DistinctBy(s => s.Name.ToNormalized()).OrderBy(r => r.Name).ToList(); + + return recDto; + } + + private async Task GetSeriesDetail(string license, int? aniListId, long? malId, int? seriesId) { var payload = new ExternalMetadataIdsDto() @@ -69,9 +389,11 @@ public class ExternalMetadataService : IExternalMetadataService SeriesName = string.Empty, LocalizedSeriesName = string.Empty }; + if (seriesId is > 0) { - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId.Value, SeriesIncludes.Metadata | SeriesIncludes.Library); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId.Value, + SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.ExternalReviews); if (series != null) { if (payload.AniListId <= 0) @@ -90,7 +412,7 @@ public class ExternalMetadataService : IExternalMetadataService } try { - return await (Configuration.KavitaPlusApiUrl + "/api/metadata/series/detail") + return await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-by-ids") .WithHeader("Accept", "application/json") .WithHeader("User-Agent", "Kavita") .WithHeader("x-license-key", license) @@ -117,6 +439,7 @@ public class ExternalMetadataService : IExternalMetadataService LibraryType.Manga => seriesFormat == MangaFormat.Epub ? MediaFormat.LightNovel : MediaFormat.Manga, LibraryType.Comic => MediaFormat.Comic, LibraryType.Book => MediaFormat.Book, + LibraryType.LightNovel => MediaFormat.LightNovel, _ => MediaFormat.Unknown }; } diff --git a/API/Services/Plus/LicenseService.cs b/API/Services/Plus/LicenseService.cs index c00527c2c..439a1aaf5 100644 --- a/API/Services/Plus/LicenseService.cs +++ b/API/Services/Plus/LicenseService.cs @@ -7,7 +7,6 @@ using API.DTOs.License; using API.Entities.Enums; using EasyCaching.Core; using Flurl.Http; -using Hangfire; using Kavita.Common; using Kavita.Common.EnvironmentInfo; using Microsoft.Extensions.Logging; @@ -24,31 +23,25 @@ internal class RegisterLicenseResponseDto public interface ILicenseService { - Task ValidateLicenseStatus(); + //Task ValidateLicenseStatus(); Task RemoveLicense(); Task AddLicense(string license, string email, string? discordId); Task HasActiveLicense(bool forceCheck = false); + Task HasActiveSubscription(string? license); Task ResetLicense(string license, string email); } -public class LicenseService : ILicenseService +public class LicenseService( + IEasyCachingProviderFactory cachingProviderFactory, + IUnitOfWork unitOfWork, + ILogger logger) + : ILicenseService { - private readonly IEasyCachingProviderFactory _cachingProviderFactory; - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; private readonly TimeSpan _licenseCacheTimeout = TimeSpan.FromHours(8); public const string Cron = "0 */4 * * *"; private const string CacheKey = "license"; - public LicenseService(IEasyCachingProviderFactory cachingProviderFactory, IUnitOfWork unitOfWork, ILogger logger) - { - _cachingProviderFactory = cachingProviderFactory; - _unitOfWork = unitOfWork; - _logger = logger; - } - - /// /// Performs license lookup to API layer /// @@ -77,7 +70,7 @@ public class LicenseService : ILicenseService } catch (Exception e) { - _logger.LogError(e, "An error happened during the request to Kavita+ API"); + logger.LogError(e, "An error happened during the request to Kavita+ API"); throw; } } @@ -115,12 +108,12 @@ public class LicenseService : ILicenseService return response.EncryptedLicense; } - _logger.LogError("An error happened during the request to Kavita+ API: {ErrorMessage}", response.ErrorMessage); + logger.LogError("An error happened during the request to Kavita+ API: {ErrorMessage}", response.ErrorMessage); throw new KavitaException(response.ErrorMessage); } catch (FlurlHttpException e) { - _logger.LogError(e, "An error happened during the request to Kavita+ API"); + logger.LogError(e, "An error happened during the request to Kavita+ API"); return string.Empty; } } @@ -129,57 +122,41 @@ public class LicenseService : ILicenseService /// Checks licenses and updates cache /// /// Expected to be called at startup and on reoccurring basis - public async Task ValidateLicenseStatus() - { - var provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); - try - { - var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); - if (string.IsNullOrEmpty(license.Value)) { - await provider.SetAsync(CacheKey, false, _licenseCacheTimeout); - return; - } - - _logger.LogInformation("Validating Kavita+ License"); - - await provider.FlushAsync(); - var isValid = await IsLicenseValid(license.Value); - await provider.SetAsync(CacheKey, isValid, _licenseCacheTimeout); - - _logger.LogInformation("Validating Kavita+ License - Complete"); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an error talking with Kavita+ API for license validation. Rescheduling check in 30 mins"); - await provider.SetAsync(CacheKey, false, _licenseCacheTimeout); - BackgroundJob.Schedule(() => ValidateLicenseStatus(), TimeSpan.FromMinutes(30)); - } - } - - public async Task RemoveLicense() - { - var serverSetting = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); - serverSetting.Value = string.Empty; - _unitOfWork.SettingsRepository.Update(serverSetting); - await _unitOfWork.CommitAsync(); - var provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); - await provider.RemoveAsync(CacheKey); - } - - public async Task AddLicense(string license, string email, string? discordId) - { - var serverSetting = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); - var lic = await RegisterLicense(license, email, discordId); - if (string.IsNullOrWhiteSpace(lic)) - throw new KavitaException("unable-to-register-k+"); - serverSetting.Value = lic; - _unitOfWork.SettingsRepository.Update(serverSetting); - await _unitOfWork.CommitAsync(); - } + // public async Task ValidateLicenseStatus() + // { + // var provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); + // try + // { + // var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + // if (string.IsNullOrEmpty(license.Value)) { + // await provider.SetAsync(CacheKey, false, _licenseCacheTimeout); + // return; + // } + // + // _logger.LogInformation("Validating Kavita+ License"); + // + // await provider.FlushAsync(); + // var isValid = await IsLicenseValid(license.Value); + // await provider.SetAsync(CacheKey, isValid, _licenseCacheTimeout); + // + // _logger.LogInformation("Validating Kavita+ License - Complete"); + // } + // catch (Exception ex) + // { + // _logger.LogError(ex, "There was an error talking with Kavita+ API for license validation. Rescheduling check in 30 mins"); + // await provider.SetAsync(CacheKey, false, _licenseCacheTimeout); + // BackgroundJob.Schedule(() => ValidateLicenseStatus(), TimeSpan.FromMinutes(30)); + // } + // } + /// + /// Checks licenses and updates cache + /// + /// Skip what's in cache + /// public async Task HasActiveLicense(bool forceCheck = false) { - var provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); + var provider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); if (!forceCheck) { var cacheValue = await provider.GetAsync(CacheKey); @@ -188,7 +165,7 @@ public class LicenseService : ILicenseService try { - var serverSetting = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + var serverSetting = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); var result = await IsLicenseValid(serverSetting.Value); await provider.FlushAsync(); await provider.SetAsync(CacheKey, result, _licenseCacheTimeout); @@ -196,17 +173,79 @@ public class LicenseService : ILicenseService } catch (Exception ex) { - _logger.LogError(ex, "There was an issue connecting to Kavita+"); + logger.LogError(ex, "There was an issue connecting to Kavita+"); + await provider.FlushAsync(); + await provider.SetAsync(CacheKey, false, _licenseCacheTimeout); } return false; } + public async Task HasActiveSubscription(string? license) + { + if (string.IsNullOrWhiteSpace(license)) return false; + try + { + var response = await (Configuration.KavitaPlusApiUrl + "/api/license/check-sub") + .WithHeader("Accept", "application/json") + .WithHeader("User-Agent", "Kavita") + .WithHeader("x-license-key", license) + .WithHeader("x-installId", HashUtil.ServerToken()) + .WithHeader("x-kavita-version", BuildInfo.Version) + .WithHeader("Content-Type", "application/json") + .WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs)) + .PostJsonAsync(new LicenseValidDto() + { + License = license, + InstallId = HashUtil.ServerToken() + }) + .ReceiveString(); + var result = bool.Parse(response); + + if (!result) + { + var provider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); + await provider.FlushAsync(); + await provider.SetAsync(CacheKey, result, _licenseCacheTimeout); + } + + return result; + } + catch (Exception e) + { + logger.LogError(e, "An error happened during the request to Kavita+ API"); + return false; + } + } + + public async Task RemoveLicense() + { + var serverSetting = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + serverSetting.Value = string.Empty; + unitOfWork.SettingsRepository.Update(serverSetting); + await unitOfWork.CommitAsync(); + var provider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); + await provider.RemoveAsync(CacheKey); + } + + public async Task AddLicense(string license, string email, string? discordId) + { + var serverSetting = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + var lic = await RegisterLicense(license, email, discordId); + if (string.IsNullOrWhiteSpace(lic)) + throw new KavitaException("unable-to-register-k+"); + serverSetting.Value = lic; + unitOfWork.SettingsRepository.Update(serverSetting); + await unitOfWork.CommitAsync(); + } + + + public async Task ResetLicense(string license, string email) { try { - var encryptedLicense = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + var encryptedLicense = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); var response = await (Configuration.KavitaPlusApiUrl + "/api/license/reset") .WithHeader("Accept", "application/json") .WithHeader("User-Agent", "Kavita") @@ -225,17 +264,17 @@ public class LicenseService : ILicenseService if (string.IsNullOrEmpty(response)) { - var provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); + var provider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); await provider.RemoveAsync(CacheKey); return true; } - _logger.LogError("An error happened during the request to Kavita+ API: {ErrorMessage}", response); + logger.LogError("An error happened during the request to Kavita+ API: {ErrorMessage}", response); throw new KavitaException(response); } catch (FlurlHttpException e) { - _logger.LogError(e, "An error happened during the request to Kavita+ API"); + logger.LogError(e, "An error happened during the request to Kavita+ API"); } return false; diff --git a/API/Services/Plus/RatingService.cs b/API/Services/Plus/RatingService.cs deleted file mode 100644 index 7701b2326..000000000 --- a/API/Services/Plus/RatingService.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.Entities; -using API.Entities.Enums; -using API.Helpers; -using API.Helpers.Builders; -using Flurl.Http; -using Kavita.Common; -using Kavita.Common.EnvironmentInfo; -using Kavita.Common.Helpers; -using Microsoft.Extensions.Logging; - -namespace API.Services.Plus; -#nullable enable - -public interface IRatingService -{ - Task> GetRatings(int seriesId); -} - -public class RatingService : IRatingService -{ - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - - public RatingService(IUnitOfWork unitOfWork, ILogger logger) - { - _unitOfWork = unitOfWork; - _logger = logger; - - FlurlHttp.ConfigureClient(Configuration.KavitaPlusApiUrl, cli => - cli.Settings.HttpClientFactory = new UntrustedCertClientFactory()); - } - - public async Task> GetRatings(int seriesId) - { - var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, - SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.Chapters | SeriesIncludes.Volumes); - - // Don't send any ratings back for Comic libraries as Kavita+ doesn't have any providers for that - if (series == null || series.Library.Type == LibraryType.Comic) return ImmutableList.Empty; - return await GetRatings(license.Value, series); - } - - private async Task> GetRatings(string license, Series series) - { - try - { - return await (Configuration.KavitaPlusApiUrl + "/api/rating") - .WithHeader("Accept", "application/json") - .WithHeader("User-Agent", "Kavita") - .WithHeader("x-license-key", license) - .WithHeader("x-installId", HashUtil.ServerToken()) - .WithHeader("x-kavita-version", BuildInfo.Version) - .WithHeader("Content-Type", "application/json") - .WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs)) - .PostJsonAsync(new PlusSeriesDtoBuilder(series).Build()) - .ReceiveJson>(); - } - catch (Exception e) - { - _logger.LogError(e, "An error happened during the request to Kavita+ API"); - } - - return new List(); - } -} diff --git a/API/Services/Plus/RecommendationService.cs b/API/Services/Plus/RecommendationService.cs index 1a6a1b315..24cb1445b 100644 --- a/API/Services/Plus/RecommendationService.cs +++ b/API/Services/Plus/RecommendationService.cs @@ -20,43 +20,13 @@ using Microsoft.Extensions.Logging; namespace API.Services.Plus; #nullable enable -public record PlusSeriesDto -{ - public int? AniListId { get; set; } - public long? MalId { get; set; } - public string? GoogleBooksId { get; set; } - public string? MangaDexId { get; set; } - public string SeriesName { get; set; } - public string? AltSeriesName { get; set; } - public MediaFormat MediaFormat { get; set; } - /// - /// Optional but can help with matching - /// - public int? ChapterCount { get; set; } - /// - /// Optional but can help with matching - /// - public int? VolumeCount { get; set; } - public int? Year { get; set; } -} - -internal record MediaRecommendationDto -{ - public int Rating { get; set; } - public IEnumerable RecommendationNames { get; set; } = null!; - public string Name { get; set; } - public string CoverUrl { get; set; } - public string SiteUrl { get; set; } - public string? Summary { get; set; } - public int? AniListId { get; set; } - public long? MalId { get; set; } -} public interface IRecommendationService { - Task GetRecommendationsForSeries(int userId, int seriesId); + //Task GetRecommendationsForSeries(int userId, int seriesId); } + public class RecommendationService : IRecommendationService { private readonly IUnitOfWork _unitOfWork; @@ -93,7 +63,7 @@ public class RecommendationService : IRecommendationService foreach (var rec in recs) { // Find the series based on name and type and that the user has access too - var seriesForRec = await _unitOfWork.SeriesRepository.GetSeriesDtoByNamesAndMetadataIdsForUser(userId, rec.RecommendationNames, + var seriesForRec = await _unitOfWork.SeriesRepository.GetSeriesDtoByNamesAndMetadataIds(rec.RecommendationNames, series.Library.Type, ScrobblingService.CreateUrl(ScrobblingService.AniListWeblinkWebsite, rec.AniListId), ScrobblingService.CreateUrl(ScrobblingService.MalWeblinkWebsite, rec.MalId)); @@ -126,7 +96,7 @@ public class RecommendationService : IRecommendationService } - private async Task> GetRecommendations(string license, Series series) + protected async Task> GetRecommendations(string license, Series series) { try { diff --git a/API/Services/Plus/ScrobblingService.cs b/API/Services/Plus/ScrobblingService.cs index 7c117debc..93d75c246 100644 --- a/API/Services/Plus/ScrobblingService.cs +++ b/API/Services/Plus/ScrobblingService.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using API.Data; using API.Data.Repositories; using API.DTOs.Filtering; -using API.DTOs.Recommendation; using API.DTOs.Scrobbling; using API.Entities; using API.Entities.Enums; @@ -53,6 +52,7 @@ public interface IScrobblingService [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] Task ProcessUpdatesSinceLastSync(); Task CreateEventsFromExistingHistory(int userId = 0); + Task ClearEventsForSeries(int userId, int seriesId); } public class ScrobblingService : IScrobblingService @@ -77,9 +77,12 @@ public class ScrobblingService : IScrobblingService {MangaDexWeblinkWebsite, 0}, }; - private const int ScrobbleSleepTime = 700; // We can likely tie this to AniList's 90 rate / min ((60 * 1000) / 90) + private const int ScrobbleSleepTime = 1000; // We can likely tie this to AniList's 90 rate / min ((60 * 1000) / 90) private static readonly IList BookProviders = new List() + { + }; + private static readonly IList LightNovelProviders = new List() { ScrobbleProvider.AniList }; @@ -89,6 +92,9 @@ public class ScrobblingService : IScrobblingService ScrobbleProvider.AniList }; + private const string UnknownSeriesErrorMessage = "Series cannot be matched for Scrobbling"; + private const string AccessTokenErrorMessage = "Access Token needs to be rotated to continue scrobbling"; + public ScrobblingService(IUnitOfWork unitOfWork, ITokenService tokenService, IEventHub eventHub, ILogger logger, ILicenseService licenseService, @@ -298,7 +304,7 @@ public class ScrobblingService : IScrobblingService var prevVol = $"{existingEvt.VolumeNumber}"; existingEvt.VolumeNumber = - await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId); + (int) await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId); existingEvt.ChapterNumber = await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(seriesId, userId); _unitOfWork.ScrobbleRepository.Update(existingEvt); @@ -319,7 +325,7 @@ public class ScrobblingService : IScrobblingService MalId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite), AppUserId = userId, VolumeNumber = - await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId), + (int) await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId), ChapterNumber = await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(seriesId, userId), Format = LibraryTypeHelper.GetFormat(series.Library.Type), @@ -374,7 +380,7 @@ public class ScrobblingService : IScrobblingService var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId); if (library is not {AllowScrobbling: true}) return true; - if (library.Type == LibraryType.Comic) return true; + if (!ExternalMetadataService.IsPlusEligible(library.Type)) return true; return false; } @@ -424,8 +430,21 @@ public class ScrobblingService : IScrobblingService if (response.ErrorMessage != null && response.ErrorMessage.Contains("Too Many Requests")) { _logger.LogInformation("Hit Too many requests, sleeping to regain requests"); - await Task.Delay(TimeSpan.FromMinutes(1)); - } else if (response.ErrorMessage != null && response.ErrorMessage.Contains("Unknown Series")) + await Task.Delay(TimeSpan.FromMinutes(10)); + } else if (response.ErrorMessage != null && response.ErrorMessage.Contains("Unauthorized")) + { + _logger.LogCritical("Kavita+ responded with Unauthorized. Please check your subscription"); + await _licenseService.HasActiveLicense(true); + evt.IsErrored = true; + evt.ErrorDetails = "Kavita+ subscription no longer active"; + throw new KavitaException("Kavita+ responded with Unauthorized. Please check your subscription"); + } else if (response.ErrorMessage != null && response.ErrorMessage.Contains("Access token is invalid")) + { + evt.IsErrored = true; + evt.ErrorDetails = AccessTokenErrorMessage; + throw new KavitaException("Access token is invalid"); + } + else if (response.ErrorMessage != null && response.ErrorMessage.Contains("Unknown Series")) { // Log the Series name and Id in ScrobbleErrors _logger.LogInformation("Kavita+ was unable to match the series"); @@ -433,12 +452,16 @@ public class ScrobblingService : IScrobblingService { _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError() { - Comment = "Unknown Series", + Comment = UnknownSeriesErrorMessage, Details = data.SeriesName, LibraryId = evt.LibraryId, SeriesId = evt.SeriesId }); + await _unitOfWork.ExternalSeriesMetadataRepository.CreateBlacklistedSeries(evt.SeriesId, false); } + + evt.IsErrored = true; + evt.ErrorDetails = UnknownSeriesErrorMessage; } else if (response.ErrorMessage != null && response.ErrorMessage.StartsWith("Review")) { // Log the Series name and Id in ScrobbleErrors @@ -453,8 +476,11 @@ public class ScrobblingService : IScrobblingService SeriesId = evt.SeriesId }); } + evt.IsErrored = true; + evt.ErrorDetails = "Review was unable to be saved due to upstream requirements"; } + evt.IsErrored = true; _logger.LogError("Scrobbling failed due to {ErrorMessage}: {SeriesName}", response.ErrorMessage, data.SeriesName); throw new KavitaException($"Scrobbling failed due to {response.ErrorMessage}: {data.SeriesName}"); } @@ -470,12 +496,14 @@ public class ScrobblingService : IScrobblingService { _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError() { - Comment = "Unknown Series", + Comment = UnknownSeriesErrorMessage, Details = data.SeriesName, LibraryId = evt.LibraryId, SeriesId = evt.SeriesId }); } + evt.IsErrored = true; + evt.ErrorDetails = "Bad payload from Scrobble Provider"; throw new KavitaException("Bad payload from Scrobble Provider"); } throw; @@ -542,6 +570,26 @@ public class ScrobblingService : IScrobblingService } } + /// + /// Removes all events (active) that are tied to a now-on hold series + /// + /// + /// + public async Task ClearEventsForSeries(int userId, int seriesId) + { + _logger.LogInformation("Clearing Pre-existing Scrobble events for Series {SeriesId} by User {UserId} as Series is now on hold list", seriesId, userId); + var events = await _unitOfWork.ScrobbleRepository.GetUserEventsForSeries(userId, seriesId); + foreach (var scrobble in events) + { + _unitOfWork.ScrobbleRepository.Remove(scrobble); + } + + await _unitOfWork.CommitAsync(); + } + + /// + /// Removes all events that have been processed that are 7 days old + /// [DisableConcurrentExecution(60 * 60 * 60)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] public async Task ClearProcessedEvents() @@ -573,11 +621,10 @@ public class ScrobblingService : IScrobblingService .ToImmutableHashSet(); var errors = (await _unitOfWork.ScrobbleRepository.GetScrobbleErrors()) - .Where(e => e.Comment == "Unknown Series") + .Where(e => e.Comment == "Unknown Series" || e.Comment == UnknownSeriesErrorMessage || e.Comment == AccessTokenErrorMessage) .Select(e => e.SeriesId) .ToList(); - var readEvents = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.ChapterRead)) .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) .Where(e => !errors.Contains(e.SeriesId)) @@ -594,10 +641,7 @@ public class ScrobblingService : IScrobblingService .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) .Where(e => !errors.Contains(e.SeriesId)) .ToList(); - var reviewEvents = (await _unitOfWork.ScrobbleRepository.GetByEvent(ScrobbleEventType.Review)) - .Where(e => librariesWithScrobbling.Contains(e.LibraryId)) - .Where(e => !errors.Contains(e.SeriesId)) - .ToList(); + var decisions = addToWantToRead .GroupBy(item => new { item.SeriesId, item.AppUserId }) .Select(group => new @@ -617,6 +661,7 @@ public class ScrobblingService : IScrobblingService .Concat(addToWantToRead.Select(r => r.AppUser)) .Concat(removeWantToRead.Select(r => r.AppUser)) .Concat(ratingEvents.Select(r => r.AppUser)) + .Where(user => !string.IsNullOrEmpty(user.AniListAccessToken)) .DistinctBy(u => u.Id) .ToList(); foreach (var user in usersToScrobble) @@ -624,7 +669,7 @@ public class ScrobblingService : IScrobblingService await SetAndCheckRateLimit(userRateLimits, user, license.Value); } - var totalProgress = readEvents.Count + decisions.Count + ratingEvents.Count + decisions.Count + reviewEvents.Count; + var totalProgress = readEvents.Count + decisions.Count + ratingEvents.Count + decisions.Count; _logger.LogInformation("Found {TotalEvents} Scrobble Events", totalProgress); try @@ -633,7 +678,7 @@ public class ScrobblingService : IScrobblingService foreach (var readEvt in readEvents) { readEvt.VolumeNumber = - await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(readEvt.SeriesId, + (int) await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(readEvt.SeriesId, readEvt.AppUser.Id); readEvt.ChapterNumber = await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(readEvt.SeriesId, @@ -647,7 +692,7 @@ public class ScrobblingService : IScrobblingService MALId = (int?) evt.MalId, ScrobbleEventType = evt.ScrobbleEventType, ChapterNumber = evt.ChapterNumber, - VolumeNumber = evt.VolumeNumber, + VolumeNumber = (int?) evt.VolumeNumber, AniListToken = evt.AppUser.AniListAccessToken, SeriesName = evt.Series.Name, LocalizedSeriesName = evt.Series.LocalizedName, @@ -671,22 +716,6 @@ public class ScrobblingService : IScrobblingService Year = evt.Series.Metadata.ReleaseYear })); - progressCounter = await ProcessEvents(reviewEvents, userRateLimits, usersToScrobble.Count, progressCounter, - totalProgress, evt => Task.FromResult(new ScrobbleDto() - { - Format = evt.Format, - AniListId = evt.AniListId, - MALId = (int?) evt.MalId, - ScrobbleEventType = evt.ScrobbleEventType, - AniListToken = evt.AppUser.AniListAccessToken, - SeriesName = evt.Series.Name, - LocalizedSeriesName = evt.Series.LocalizedName, - Rating = evt.Rating, - Year = evt.Series.Metadata.ReleaseYear, - ReviewBody = evt.ReviewBody, - ReviewTitle = evt.ReviewTitle - })); - progressCounter = await ProcessEvents(decisions, userRateLimits, usersToScrobble.Count, progressCounter, totalProgress, evt => Task.FromResult(new ScrobbleDto() { @@ -695,7 +724,7 @@ public class ScrobblingService : IScrobblingService MALId = (int?) evt.MalId, ScrobbleEventType = evt.ScrobbleEventType, ChapterNumber = evt.ChapterNumber, - VolumeNumber = evt.VolumeNumber, + VolumeNumber = (int?) evt.VolumeNumber, AniListToken = evt.AppUser.AniListAccessToken, SeriesName = evt.Series.Name, LocalizedSeriesName = evt.Series.LocalizedName, @@ -745,7 +774,39 @@ public class ScrobblingService : IScrobblingService { continue; } + + if (_tokenService.HasTokenExpired(evt.AppUser.AniListAccessToken)) + { + _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError() + { + Comment = "AniList token has expired and needs rotating. Scrobbles wont work until then", + Details = $"User: {evt.AppUser.UserName}", + LibraryId = evt.LibraryId, + SeriesId = evt.SeriesId + }); + await _unitOfWork.CommitAsync(); + return 0; + } + + if (await _unitOfWork.ExternalSeriesMetadataRepository.IsBlacklistedSeries(evt.SeriesId)) + { + _unitOfWork.ScrobbleRepository.Attach(new ScrobbleError() + { + Comment = UnknownSeriesErrorMessage, + Details = $"User: {evt.AppUser.UserName} Series: {evt.Series.Name}", + LibraryId = evt.LibraryId, + SeriesId = evt.SeriesId + }); + evt.IsErrored = true; + evt.ErrorDetails = "Series cannot be matched for Scrobbling"; + evt.ProcessDateUtc = DateTime.UtcNow; + _unitOfWork.ScrobbleRepository.Update(evt); + await _unitOfWork.CommitAsync(); + return 0; + } + var count = await SetAndCheckRateLimit(userRateLimits, evt.AppUser, license.Value); + userRateLimits[evt.AppUserId] = count; if (count == 0) { if (usersToScrobble == 1) break; @@ -765,6 +826,17 @@ public class ScrobblingService : IScrobblingService // If a flurl exception occured, the API is likely down. Kill processing throw; } + catch (KavitaException ex) + { + if (ex.Message.Contains("Access token is invalid")) + { + _logger.LogCritical("Access Token for UserId: {UserId} needs to be rotated to continue scrobbling", evt.AppUser.Id); + evt.IsErrored = true; + evt.ErrorDetails = AccessTokenErrorMessage; + _unitOfWork.ScrobbleRepository.Update(evt); + return progressCounter; + } + } catch (Exception) { /* Swallow as it's already been handled in PostScrobbleUpdate */ @@ -808,6 +880,12 @@ public class ScrobblingService : IScrobblingService return true; } + if (readEvent.Series.Library.Type == LibraryType.LightNovel && + LightNovelProviders.Intersect(userProviders).Any()) + { + return true; + } + return false; } @@ -853,6 +931,7 @@ public class ScrobblingService : IScrobblingService private async Task SetAndCheckRateLimit(IDictionary userRateLimits, AppUser user, string license) { + if (string.IsNullOrEmpty(user.AniListAccessToken)) return 0; try { if (!userRateLimits.ContainsKey(user.Id)) diff --git a/API/Services/ReaderService.cs b/API/Services/ReaderService.cs index be46fc90b..19548e0a6 100644 --- a/API/Services/ReaderService.cs +++ b/API/Services/ReaderService.cs @@ -365,18 +365,17 @@ public class ReaderService : IReaderService var currentVolume = volumes.Single(v => v.Id == volumeId); var currentChapter = currentVolume.Chapters.Single(c => c.Id == currentChapterId); - if (currentVolume.Number == 0) + if (currentVolume.MinNumber == 0) { // Handle specials by sorting on their Filename aka Range var chapterId = GetNextChapterId(currentVolume.Chapters.OrderByNatural(x => x.Range), currentChapter.Range, dto => dto.Range); if (chapterId > 0) return chapterId; } - var currentVolumeNumber = currentVolume.Name.AsFloat(); var next = false; foreach (var volume in volumes) { - var volumeNumbersMatch = Math.Abs(volume.Name.AsFloat() - currentVolumeNumber) < 0.00001f; + var volumeNumbersMatch = volume.Name == currentVolume.Name; if (volumeNumbersMatch && volume.Chapters.Count > 1) { // Handle Chapters within current Volume @@ -420,9 +419,9 @@ public class ReaderService : IReaderService else if (firstChapter.Number.AsDouble() == 0) return firstChapter.Id; // If on last volume AND there are no specials left, then let's return -1 - var anySpecials = volumes.Where(v => $"{v.Number}" == Parser.DefaultVolume) + var anySpecials = volumes.Where(v => $"{v.MinNumber}" == Parser.DefaultVolume) .SelectMany(v => v.Chapters.Where(c => c.IsSpecial)).Any(); - if (currentVolume.Number != 0 && !anySpecials) + if (currentVolume.MinNumber != 0 && !anySpecials) { return -1; } @@ -434,10 +433,10 @@ public class ReaderService : IReaderService // This has an added problem that it will loop up to the beginning always // Should I change this to Max number? volumes.LastOrDefault()?.Number -> volumes.Max(v => v.Number) - if (currentVolume.Number != 0 && currentVolume.Number == volumes.LastOrDefault()?.Number && volumes.Count > 1) + if (currentVolume.MinNumber != 0 && currentVolume.MinNumber == volumes.LastOrDefault()?.MinNumber && volumes.Count > 1) { var chapterVolume = volumes.FirstOrDefault(); - if (chapterVolume?.Number != 0) return -1; + if (chapterVolume?.MinNumber != 0) return -1; // This is my attempt at fixing a bug where we loop around to the beginning, but I just can't seem to figure it out // var orderedVolumes = volumes.OrderBy(v => v.Number, SortComparerZeroLast.Default).ToList(); @@ -479,7 +478,7 @@ public class ReaderService : IReaderService var currentVolume = volumes.Single(v => v.Id == volumeId); var currentChapter = currentVolume.Chapters.Single(c => c.Id == currentChapterId); - if (currentVolume.Number == 0) + if (currentVolume.MinNumber == 0) { var chapterId = GetNextChapterId(currentVolume.Chapters.OrderByNatural(x => x.Range).Reverse(), currentChapter.Range, dto => dto.Range); @@ -489,7 +488,7 @@ public class ReaderService : IReaderService var next = false; foreach (var volume in volumes) { - if (volume.Number == currentVolume.Number) + if (volume.MinNumber == currentVolume.MinNumber) { var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => x.Number.AsDouble(), _chapterSortComparerForInChapterSorting).Reverse(), currentChapter.Range, dto => dto.Range); @@ -499,15 +498,15 @@ public class ReaderService : IReaderService } if (next) { - if (currentVolume.Number - 1 == 0) break; // If we have walked all the way to chapter volume, then we should break so logic outside can work + if (currentVolume.MinNumber - 1 == 0) break; // If we have walked all the way to chapter volume, then we should break so logic outside can work var lastChapter = volume.Chapters.MaxBy(x => x.Number.AsDouble(), _chapterSortComparerForInChapterSorting); if (lastChapter == null) return -1; return lastChapter.Id; } } - var lastVolume = volumes.MaxBy(v => v.Number); - if (currentVolume.Number == 0 && currentVolume.Number != lastVolume?.Number && lastVolume?.Chapters.Count > 1) + var lastVolume = volumes.MaxBy(v => v.MinNumber); + if (currentVolume.MinNumber == 0 && currentVolume.MinNumber != lastVolume?.MinNumber && lastVolume?.Chapters.Count > 1) { var lastChapter = lastVolume.Chapters.MaxBy(x => x.Number.AsDouble(), _chapterSortComparerForInChapterSorting); if (lastChapter == null) return -1; @@ -532,13 +531,29 @@ public class ReaderService : IReaderService if (!await _unitOfWork.AppUserProgressRepository.AnyUserProgressForSeriesAsync(seriesId, userId)) { // I think i need a way to sort volumes last - return volumes.OrderBy(v => v.Number.ToString(CultureInfo.InvariantCulture).AsDouble(), _chapterSortComparer).First().Chapters - .OrderBy(c => c.Number.AsFloat()).First(); + var chapters = volumes.OrderBy(v => v.MinNumber, _chapterSortComparer).First().Chapters + .OrderBy(c => c.Number.AsFloat()) + .ToList(); + + // If there are specials, then return the first Non-special + if (chapters.Exists(c => c.IsSpecial)) + { + var firstChapter = chapters.FirstOrDefault(c => !c.IsSpecial); + if (firstChapter == null) + { + // If there is no non-special chapter, then return first chapter + return chapters[0]; + } + + return firstChapter; + } + // Else use normal logic + return chapters[0]; } // Loop through all chapters that are not in volume 0 var volumeChapters = volumes - .Where(v => v.Number != 0) + .Where(v => v.MinNumber != 0) .SelectMany(v => v.Chapters) .ToList(); @@ -550,7 +565,7 @@ public class ReaderService : IReaderService if (currentlyReadingChapter != null) return currentlyReadingChapter; // Order with volume 0 last so we prefer the natural order - return FindNextReadingChapter(volumes.OrderBy(v => v.Number, SortComparerZeroLast.Default) + return FindNextReadingChapter(volumes.OrderBy(v => v.MinNumber, SortComparerZeroLast.Default) .SelectMany(v => v.Chapters.OrderBy(c => c.Number.AsDouble())) .ToList()); } @@ -619,7 +634,7 @@ public class ReaderService : IReaderService public async Task MarkChaptersUntilAsRead(AppUser user, int seriesId, float chapterNumber) { var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(new List { seriesId }, true); - foreach (var volume in volumes.OrderBy(v => v.Number)) + foreach (var volume in volumes.OrderBy(v => v.MinNumber)) { var chapters = volume.Chapters .Where(c => !c.IsSpecial && Parser.MaxNumberFromRange(c.Range) <= chapterNumber) @@ -631,7 +646,7 @@ public class ReaderService : IReaderService public async Task MarkVolumesUntilAsRead(AppUser user, int seriesId, int volumeNumber) { var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(new List { seriesId }, true); - foreach (var volume in volumes.Where(v => v.Number <= volumeNumber && v.Number > 0).OrderBy(v => v.Number)) + foreach (var volume in volumes.Where(v => v.MinNumber <= volumeNumber && v.MinNumber > 0).OrderBy(v => v.MinNumber)) { await MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters); } @@ -761,6 +776,7 @@ public class ReaderService : IReaderService } return "Issue" + (includeSpace ? " " : string.Empty); case LibraryType.Book: + case LibraryType.LightNovel: return "Book" + (includeSpace ? " " : string.Empty); default: throw new ArgumentOutOfRangeException(nameof(libraryType), libraryType, null); diff --git a/API/Services/ReadingListService.cs b/API/Services/ReadingListService.cs index d5d5d3152..4f12f9df1 100644 --- a/API/Services/ReadingListService.cs +++ b/API/Services/ReadingListService.cs @@ -633,7 +633,7 @@ public class ReadingListService : IReadingListService var bookVolume = string.IsNullOrEmpty(book.Volume) ? Parser.DefaultVolume : book.Volume; - var matchingVolume = bookSeries.Volumes.Find(v => bookVolume == v.Name) ?? bookSeries.Volumes.Find(v => v.Number == 0); + var matchingVolume = bookSeries.Volumes.Find(v => bookVolume == v.Name) ?? bookSeries.Volumes.Find(v => v.MinNumber == 0); if (matchingVolume == null) { importSummary.Results.Add(new CblBookResult(book) diff --git a/API/Services/ReviewService.cs b/API/Services/ReviewService.cs index 6ad170df8..69ab784ae 100644 --- a/API/Services/ReviewService.cs +++ b/API/Services/ReviewService.cs @@ -2,89 +2,50 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; using API.DTOs.SeriesDetail; -using API.Entities; -using API.Entities.Enums; -using API.Helpers; -using API.Helpers.Builders; -using API.Services.Plus; -using Flurl.Http; using HtmlAgilityPack; -using Kavita.Common; -using Kavita.Common.EnvironmentInfo; -using Kavita.Common.Helpers; -using Microsoft.Extensions.Logging; + namespace API.Services; -internal class MediaReviewDto +public static class ReviewService { - public string Body { get; set; } - public string Tagline { get; set; } - public int Rating { get; set; } - public int TotalVotes { get; set; } - /// - /// The media's overall Score - /// - public int Score { get; set; } - public string SiteUrl { get; set; } - /// - /// In Markdown - /// - public string RawBody { get; set; } - public string Username { get; set; } - public ScrobbleProvider Provider { get; set; } -} - -public interface IReviewService -{ - Task> GetReviewsForSeries(int userId, int seriesId); -} - -public class ReviewService : IReviewService -{ - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - - - public ReviewService(IUnitOfWork unitOfWork, ILogger logger) + public static IEnumerable SelectSpectrumOfReviews(IList reviews) { - _unitOfWork = unitOfWork; - _logger = logger; + IList externalReviews; + var totalReviews = reviews.Count; - FlurlHttp.ConfigureClient(Configuration.KavitaPlusApiUrl, cli => - cli.Settings.HttpClientFactory = new UntrustedCertClientFactory()); - } - - public async Task> GetReviewsForSeries(int userId, int seriesId) - { - var series = - await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, - SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.Chapters | SeriesIncludes.Volumes); - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - if (user == null || series == null) return new List(); - var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); - var ret = (await GetReviews(license.Value, series)).Select(r => new UserReviewDto() + if (totalReviews > 10) { - Body = r.Body, - Tagline = r.Tagline, - Score = r.Score, - Username = r.Username, - LibraryId = series.LibraryId, - SeriesId = series.Id, - IsExternal = true, - Provider = r.Provider, - BodyJustText = GetCharacters(r.Body), - ExternalUrl = r.SiteUrl - }); + var stepSize = Math.Max((totalReviews - 4) / 8, 1); - return ret.OrderByDescending(r => r.Score); + var selectedReviews = new List() + { + reviews[0], + reviews[1], + }; + for (var i = 2; i < totalReviews - 2; i += stepSize) + { + selectedReviews.Add(reviews[i]); + + if (selectedReviews.Count >= 8) + break; + } + + selectedReviews.Add(reviews[totalReviews - 2]); + selectedReviews.Add(reviews[totalReviews - 1]); + + externalReviews = selectedReviews; + } + else + { + externalReviews = reviews; + } + + return externalReviews.OrderByDescending(r => r.Score); } - private static string GetCharacters(string body) + public static string GetCharacters(string body) { if (string.IsNullOrEmpty(body)) return body; @@ -120,29 +81,4 @@ public class ReviewService : IReviewService return plainText + "…"; } - - private async Task> GetReviews(string license, Series series) - { - _logger.LogDebug("Fetching external reviews for Series: {SeriesName}", series.Name); - try - { - return await (Configuration.KavitaPlusApiUrl + "/api/review") - .WithHeader("Accept", "application/json") - .WithHeader("User-Agent", "Kavita") - .WithHeader("x-license-key", license) - .WithHeader("x-installId", HashUtil.ServerToken()) - .WithHeader("x-kavita-version", BuildInfo.Version) - .WithHeader("Content-Type", "application/json") - .WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs)) - .PostJsonAsync(new PlusSeriesDtoBuilder(series).Build()) - .ReceiveJson>(); - - } - catch (Exception e) - { - _logger.LogError(e, "An error happened during the request to Kavita+ API"); - } - - return new List(); - } } diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index b5d41ac52..b7556655f 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -5,6 +5,8 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using API.Comparators; +using API.Constants; +using API.Controllers; using API.Data; using API.Data.Repositories; using API.DTOs; @@ -19,6 +21,7 @@ using API.Helpers.Builders; using API.Services.Plus; using API.Services.Tasks.Scanner.Parser; using API.SignalR; +using EasyCaching.Core; using Hangfire; using Kavita.Common; using Microsoft.Extensions.Logging; @@ -68,7 +71,6 @@ public class SeriesService : ISeriesService _logger = logger; _scrobblingService = scrobblingService; _localizationService = localizationService; - } /// @@ -92,7 +94,7 @@ public class SeriesService : ISeriesService .FirstOrDefault(); if (minVolumeNumber != null && minChapter != null && float.TryParse(minChapter.Number, CultureInfo.InvariantCulture, out var chapNum) && - (chapNum >= minVolumeNumber.Number || chapNum == 0)) + (chapNum >= minVolumeNumber.MinNumber || chapNum == 0)) { return minVolumeNumber.Chapters.MinBy(c => c.Number.AsFloat(), ChapterSortComparer.Default); } @@ -100,6 +102,11 @@ public class SeriesService : ISeriesService return minChapter; } + /// + /// Updates the Series Metadata. + /// + /// + /// public async Task UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto) { try @@ -299,13 +306,11 @@ public class SeriesService : ISeriesService _logger.LogError(ex, "There was an issue cleaning up DB entries. This may happen if Komf is spamming updates. Nightly cleanup will work"); } - if (updateSeriesMetadataDto.CollectionTags == null) return true; foreach (var tag in updateSeriesMetadataDto.CollectionTags) { await _eventHub.SendMessageAsync(MessageFactory.SeriesAddedToCollection, - MessageFactory.SeriesAddedToCollectionEvent(tag.Id, - updateSeriesMetadataDto.SeriesMetadata.SeriesId), false); + MessageFactory.SeriesAddedToCollectionEvent(tag.Id, seriesId), false); } return true; } @@ -422,6 +427,9 @@ public class SeriesService : ISeriesService } var series = await _unitOfWork.SeriesRepository.GetSeriesByIdsAsync(seriesIds); + + _unitOfWork.SeriesRepository.Remove(series); + var libraryIds = series.Select(s => s.LibraryId); var libraries = await _unitOfWork.LibraryRepository.GetLibraryForIdsAsync(libraryIds); foreach (var library in libraries) @@ -429,11 +437,8 @@ public class SeriesService : ISeriesService library.UpdateLastModified(); _unitOfWork.LibraryRepository.Update(library); } + await _unitOfWork.CommitAsync(); - _unitOfWork.SeriesRepository.Remove(series); - - - if (!_unitOfWork.HasChanges() || !await _unitOfWork.CommitAsync()) return true; foreach (var s in series) { @@ -444,14 +449,13 @@ public class SeriesService : ISeriesService await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters(); await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries(); _taskScheduler.CleanupChapters(allChapterIds.ToArray()); + return true; } catch (Exception ex) { _logger.LogError(ex, "There was an issue when trying to delete multiple series"); return false; } - - return true; } /// @@ -484,7 +488,7 @@ public class SeriesService : ISeriesService // For books, the Name of the Volume is remapped to the actual name of the book, rather than Volume number. var processedVolumes = new List(); - if (libraryType == LibraryType.Book) + if (libraryType is LibraryType.Book or LibraryType.LightNovel) { var volumeLabel = await _localizationService.Translate(userId, "volume-num", string.Empty); foreach (var volume in volumes) @@ -501,7 +505,7 @@ public class SeriesService : ISeriesService } else { - processedVolumes = volumes.Where(v => v.Number > 0).ToList(); + processedVolumes = volumes.Where(v => v.MinNumber > 0).ToList(); processedVolumes.ForEach(v => { v.Name = $"Volume {v.Name}"; @@ -512,7 +516,7 @@ public class SeriesService : ISeriesService var specials = new List(); var chapters = volumes.SelectMany(v => v.Chapters.Select(c => { - if (v.Number == 0) return c; + if (v.MinNumber == 0) return c; c.VolumeTitle = v.Name; return c; }).OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default)).ToList(); @@ -528,7 +532,7 @@ public class SeriesService : ISeriesService // Don't show chapter 0 (aka single volume chapters) in the Chapters tab or books that are just single numbers (they show as volumes) IEnumerable retChapters; - if (libraryType == LibraryType.Book) + if (libraryType is LibraryType.Book or LibraryType.LightNovel) { retChapters = Array.Empty(); } else @@ -538,7 +542,7 @@ public class SeriesService : ISeriesService } var storylineChapters = volumes - .Where(v => v.Number == 0) + .Where(v => v.MinNumber == 0) .SelectMany(v => v.Chapters.Where(c => !c.IsSpecial)) .OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default) .ToList(); @@ -571,7 +575,7 @@ public class SeriesService : ISeriesService public static void RenameVolumeName(ChapterDto firstChapter, VolumeDto volume, LibraryType libraryType, string volumeLabel = "Volume") { - if (libraryType == LibraryType.Book) + if (libraryType is LibraryType.Book or LibraryType.LightNovel) { if (string.IsNullOrEmpty(firstChapter.TitleName)) { @@ -582,6 +586,7 @@ public class SeriesService : ISeriesService } else if (volume.Name != "0") { + // If the titleName has Volume inside it, let's just send that back? volume.Name += $" - {firstChapter.TitleName}"; } // else @@ -609,6 +614,7 @@ public class SeriesService : ISeriesService return libraryType switch { LibraryType.Book => await _localizationService.Translate(userId, "book-num", chapterTitle), + LibraryType.LightNovel => await _localizationService.Translate(userId, "book-num", chapterTitle), LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", hashSpot, chapterTitle), LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", chapterTitle), _ => await _localizationService.Translate(userId, "chapter-num", ' ') @@ -631,6 +637,7 @@ public class SeriesService : ISeriesService return (libraryType switch { LibraryType.Book => await _localizationService.Translate(userId, "book-num", string.Empty), + LibraryType.LightNovel => await _localizationService.Translate(userId, "book-num", string.Empty), LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", hashSpot, string.Empty), LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", string.Empty), _ => await _localizationService.Translate(userId, "chapter-num", ' ') @@ -718,7 +725,8 @@ public class SeriesService : ISeriesService { throw new UnauthorizedAccessException("user-no-access-library-from-series"); } - if (series.Metadata.PublicationStatus is not (PublicationStatus.OnGoing or PublicationStatus.Ended) || series.Library.Type == LibraryType.Book) + if (series.Metadata.PublicationStatus is not (PublicationStatus.OnGoing or PublicationStatus.Ended) || + (series.Library.Type is LibraryType.Book or LibraryType.LightNovel)) { return _emptyExpectedChapter; } @@ -779,7 +787,7 @@ public class SeriesService : ISeriesService float.TryParse(lastChapter.Number, NumberStyles.Number, CultureInfo.InvariantCulture, out var lastChapterNumber); - var lastVolumeNum = chapters.Select(c => c.Volume.Number).Max(); + var lastVolumeNum = chapters.Select(c => c.Volume.MinNumber).Max(); var result = new NextExpectedChapterDto { @@ -792,12 +800,13 @@ public class SeriesService : ISeriesService if (lastChapterNumber > 0) { result.ChapterNumber = (int) Math.Truncate(lastChapterNumber) + 1; - result.VolumeNumber = lastChapter.Volume.Number; + result.VolumeNumber = lastChapter.Volume.MinNumber; result.Title = series.Library.Type switch { LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", result.ChapterNumber), LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", "#", result.ChapterNumber), LibraryType.Book => await _localizationService.Translate(userId, "book-num", result.ChapterNumber), + LibraryType.LightNovel => await _localizationService.Translate(userId, "book-num", result.ChapterNumber), _ => await _localizationService.Translate(userId, "chapter-num", result.ChapterNumber) }; } diff --git a/API/Services/StatisticService.cs b/API/Services/StatisticService.cs index ffc2196d0..6e39e76a3 100644 --- a/API/Services/StatisticService.cs +++ b/API/Services/StatisticService.cs @@ -287,7 +287,7 @@ public class StatisticService : IStatisticService TotalPeople = distinctPeople, TotalSize = await _context.MangaFile.SumAsync(m => m.Bytes), TotalTags = await _context.Tag.CountAsync(), - VolumeCount = await _context.Volume.Where(v => v.Number != 0).CountAsync(), + VolumeCount = await _context.Volume.Where(v => v.MinNumber != 0).CountAsync(), MostActiveUsers = mostActiveUsers, MostActiveLibraries = mostActiveLibrary, MostPopularSeries = mostPopularSeries, @@ -595,7 +595,8 @@ public class StatisticService : IStatisticService { UserId = userId, Username = users.First(u => u.Id == userId).UserName, - BooksTime = user[userId].TryGetValue(LibraryType.Book, out var bookTime) ? bookTime : 0, + BooksTime = user[userId].TryGetValue(LibraryType.Book, out var bookTime) ? bookTime : 0 + + (user[userId].TryGetValue(LibraryType.LightNovel, out var bookTime2) ? bookTime2 : 0), ComicsTime = user[userId].TryGetValue(LibraryType.Comic, out var comicTime) ? comicTime : 0, MangaTime = user[userId].TryGetValue(LibraryType.Manga, out var mangaTime) ? mangaTime : 0, }) diff --git a/API/Services/TachiyomiService.cs b/API/Services/TachiyomiService.cs index e4e9aab21..68d4bb5ac 100644 --- a/API/Services/TachiyomiService.cs +++ b/API/Services/TachiyomiService.cs @@ -9,6 +9,7 @@ using System.Linq; using API.Comparators; using API.Entities; using API.Extensions; +using API.Services.Tasks.Scanner.Parser; using AutoMapper; using Microsoft.Extensions.Logging; @@ -68,21 +69,21 @@ public class TachiyomiService : ITachiyomiService // Else return the max chapter to Tachiyomi so it can consider everything read var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(seriesId)).ToImmutableList(); - var looseLeafChapterVolume = volumes.Find(v => v.Number == 0); + var looseLeafChapterVolume = volumes.Find(v => v.MinNumber == 0); if (looseLeafChapterVolume == null) { var volumeChapter = _mapper.Map(volumes [^1].Chapters .OrderBy(c => c.Number.AsFloat(), ChapterSortComparerZeroFirst.Default) .Last()); - if (volumeChapter.Number == "0") + if (volumeChapter.Number == Parser.DefaultVolume) { var volume = volumes.First(v => v.Id == volumeChapter.VolumeId); return new ChapterDto() { // Use R to ensure that localization of underlying system doesn't affect the stringification // https://docs.microsoft.com/en-us/globalization/locale/number-formatting-in-dotnet-framework - Number = (volume.Number / 10_000f).ToString("R", EnglishCulture) + Number = (volume.MinNumber / 10_000f).ToString("R", EnglishCulture) }; } @@ -103,14 +104,14 @@ public class TachiyomiService : ITachiyomiService var volumeWithProgress = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(prevChapter.VolumeId, userId); // We only encode for single-file volumes - if (volumeWithProgress!.Number != 0 && volumeWithProgress.Chapters.Count == 1) + if (volumeWithProgress!.MinNumber != 0 && volumeWithProgress.Chapters.Count == 1) { // The progress is on a volume, encode it as a fake chapterDTO return new ChapterDto() { // Use R to ensure that localization of underlying system doesn't affect the stringification // https://docs.microsoft.com/en-us/globalization/locale/number-formatting-in-dotnet-framework - Number = (volumeWithProgress.Number / 10_000f).ToString("R", EnglishCulture) + Number = (volumeWithProgress.MinNumber / 10_000f).ToString("R", EnglishCulture) }; } diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index 14f24b30e..079c28fce 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -19,6 +19,7 @@ public interface ITaskScheduler Task ScheduleTasks(); Task ScheduleStatsTasks(); void ScheduleUpdaterTasks(); + Task ScheduleKavitaPlusTasks(); void ScanFolder(string folderPath, TimeSpan delay); void ScanFolder(string folderPath); void ScanLibrary(int libraryId, bool force = false); @@ -34,6 +35,7 @@ public interface ITaskScheduler void ScanSiteThemes(); void CovertAllCoversToEncoding(); Task CleanupDbEntries(); + Task CheckForUpdate(); } public class TaskScheduler : ITaskScheduler @@ -54,6 +56,7 @@ public class TaskScheduler : ITaskScheduler private readonly IMediaConversionService _mediaConversionService; private readonly IScrobblingService _scrobblingService; private readonly ILicenseService _licenseService; + private readonly IExternalMetadataService _externalMetadataService; public static BackgroundJobServer Client => new (); public const string ScanQueue = "scan"; @@ -66,13 +69,14 @@ public class TaskScheduler : ITaskScheduler public const string BackupTaskId = "backup"; public const string ScanLibrariesTaskId = "scan-libraries"; public const string ReportStatsTaskId = "report-stats"; - public const string CheckScrobblingTokens = "check-scrobbling-tokens"; - public const string ProcessScrobblingEvents = "process-scrobbling-events"; - public const string ProcessProcessedScrobblingEvents = "process-processed-scrobbling-events"; - public const string LicenseCheck = "license-check"; + public const string CheckScrobblingTokensId = "check-scrobbling-tokens"; + public const string ProcessScrobblingEventsId = "process-scrobbling-events"; + public const string ProcessProcessedScrobblingEventsId = "process-processed-scrobbling-events"; + public const string LicenseCheckId = "license-check"; + public const string KavitaPlusDataRefreshId = "kavita+-data-refresh"; private static readonly ImmutableArray ScanTasks = - ImmutableArray.Create("ScannerService", "ScanLibrary", "ScanLibraries", "ScanFolder", "ScanSeries"); + ["ScannerService", "ScanLibrary", "ScanLibraries", "ScanFolder", "ScanSeries"]; private static readonly Random Rnd = new Random(); @@ -86,7 +90,8 @@ public class TaskScheduler : ITaskScheduler IUnitOfWork unitOfWork, IMetadataService metadataService, IBackupService backupService, ICleanupService cleanupService, IStatsService statsService, IVersionUpdaterService versionUpdaterService, IThemeService themeService, IWordCountAnalyzerService wordCountAnalyzerService, IStatisticService statisticService, - IMediaConversionService mediaConversionService, IScrobblingService scrobblingService, ILicenseService licenseService) + IMediaConversionService mediaConversionService, IScrobblingService scrobblingService, ILicenseService licenseService, + IExternalMetadataService externalMetadataService) { _cacheService = cacheService; _logger = logger; @@ -103,6 +108,7 @@ public class TaskScheduler : ITaskScheduler _mediaConversionService = mediaConversionService; _scrobblingService = scrobblingService; _licenseService = licenseService; + _externalMetadataService = externalMetadataService; } public async Task ScheduleTasks() @@ -119,7 +125,8 @@ public class TaskScheduler : ITaskScheduler } else { - RecurringJob.AddOrUpdate(ScanLibrariesTaskId, () => ScanLibraries(false), Cron.Daily, RecurringJobOptions); + RecurringJob.AddOrUpdate(ScanLibrariesTaskId, () => ScanLibraries(false), + Cron.Daily, RecurringJobOptions); } setting = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskBackup)).Value; @@ -132,26 +139,53 @@ public class TaskScheduler : ITaskScheduler // Override daily and make 2am so that everything on system has cleaned up and no blocking schedule = Cron.Daily(2); } - RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(), () => schedule, RecurringJobOptions); + RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(), + () => schedule, RecurringJobOptions); } else { - RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(), Cron.Weekly, RecurringJobOptions); + RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(), + Cron.Weekly, RecurringJobOptions); } - RecurringJob.AddOrUpdate(CleanupTaskId, () => _cleanupService.Cleanup(), Cron.Daily, RecurringJobOptions); - RecurringJob.AddOrUpdate(RemoveFromWantToReadTaskId, () => _cleanupService.CleanupWantToRead(), Cron.Daily, RecurringJobOptions); - RecurringJob.AddOrUpdate(UpdateYearlyStatsTaskId, () => _statisticService.UpdateServerStatistics(), Cron.Monthly, RecurringJobOptions); + setting = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskCleanup)).Value; + _logger.LogDebug("Scheduling Cleanup Task for {Setting}", setting); + RecurringJob.AddOrUpdate(CleanupTaskId, () => _cleanupService.Cleanup(), + CronConverter.ConvertToCronNotation(setting), RecurringJobOptions); + RecurringJob.AddOrUpdate(RemoveFromWantToReadTaskId, () => _cleanupService.CleanupWantToRead(), + Cron.Daily, RecurringJobOptions); + RecurringJob.AddOrUpdate(UpdateYearlyStatsTaskId, () => _statisticService.UpdateServerStatistics(), + Cron.Monthly, RecurringJobOptions); + + await ScheduleKavitaPlusTasks(); + } + + public async Task ScheduleKavitaPlusTasks() + { // KavitaPlus based (needs license check) - RecurringJob.AddOrUpdate(CheckScrobblingTokens, () => _scrobblingService.CheckExternalAccessTokens(), Cron.Daily, RecurringJobOptions); + var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; + if (string.IsNullOrEmpty(license) || !await _licenseService.HasActiveSubscription(license)) + { + return; + } + RecurringJob.AddOrUpdate(CheckScrobblingTokensId, () => _scrobblingService.CheckExternalAccessTokens(), + Cron.Daily, RecurringJobOptions); BackgroundJob.Enqueue(() => _scrobblingService.CheckExternalAccessTokens()); // We also kick off an immediate check on startup - RecurringJob.AddOrUpdate(LicenseCheck, () => _licenseService.ValidateLicenseStatus(), LicenseService.Cron, RecurringJobOptions); - BackgroundJob.Enqueue(() => _licenseService.ValidateLicenseStatus()); + RecurringJob.AddOrUpdate(LicenseCheckId, () => _licenseService.HasActiveLicense(true), + LicenseService.Cron, RecurringJobOptions); + BackgroundJob.Enqueue(() => _licenseService.HasActiveLicense(true)); // KavitaPlus Scrobbling (every 4 hours) - RecurringJob.AddOrUpdate(ProcessScrobblingEvents, () => _scrobblingService.ProcessUpdatesSinceLastSync(), "0 */4 * * *", RecurringJobOptions); - RecurringJob.AddOrUpdate(ProcessProcessedScrobblingEvents, () => _scrobblingService.ClearProcessedEvents(), Cron.Daily, RecurringJobOptions); + RecurringJob.AddOrUpdate(ProcessScrobblingEventsId, () => _scrobblingService.ProcessUpdatesSinceLastSync(), + "0 */4 * * *", RecurringJobOptions); + RecurringJob.AddOrUpdate(ProcessProcessedScrobblingEventsId, () => _scrobblingService.ClearProcessedEvents(), + Cron.Daily, RecurringJobOptions); + + // Backfilling/Freshening Reviews/Rating/Recommendations + RecurringJob.AddOrUpdate(KavitaPlusDataRefreshId, + () => _externalMetadataService.FetchExternalDataTask(), Cron.Daily(Rnd.Next(1, 4)), + RecurringJobOptions); } #region StatsTasks @@ -231,14 +265,14 @@ public class TaskScheduler : ITaskScheduler public void ScheduleUpdaterTasks() { _logger.LogInformation("Scheduling Auto-Update tasks"); - RecurringJob.AddOrUpdate(CheckForUpdateId, () => CheckForUpdate(), $"0 */{Rnd.Next(4, 6)} * * *", RecurringJobOptions); + RecurringJob.AddOrUpdate(CheckForUpdateId, () => CheckForUpdate(), $"0 */{Rnd.Next(1, 2)} * * *", RecurringJobOptions); BackgroundJob.Enqueue(() => CheckForUpdate()); } public void ScanFolder(string folderPath, TimeSpan delay) { var normalizedFolder = Tasks.Scanner.Parser.Parser.NormalizePath(folderPath); - if (HasAlreadyEnqueuedTask(ScannerService.Name, "ScanFolder", new object[] { normalizedFolder })) + if (HasAlreadyEnqueuedTask(ScannerService.Name, "ScanFolder", [normalizedFolder])) { _logger.LogInformation("Skipped scheduling ScanFolder for {Folder} as a job already queued", normalizedFolder); diff --git a/API/Services/Tasks/CleanupService.cs b/API/Services/Tasks/CleanupService.cs index 3d657a929..3aaa2c837 100644 --- a/API/Services/Tasks/CleanupService.cs +++ b/API/Services/Tasks/CleanupService.cs @@ -288,8 +288,8 @@ public class CleanupService : ICleanupService var seriesIds = series.Select(s => s.Id).ToList(); if (seriesIds.Count == 0) continue; - user.WantToRead ??= new List(); - user.WantToRead = user.WantToRead.Where(s => !seriesIds.Contains(s.Id)).ToList(); + user.WantToRead ??= new List(); + user.WantToRead = user.WantToRead.Where(s => !seriesIds.Contains(s.SeriesId)).ToList(); _unitOfWork.UserRepository.Update(user); } diff --git a/API/Services/Tasks/Scanner/Parser/DefaultParser.cs b/API/Services/Tasks/Scanner/Parser/DefaultParser.cs index ce41a593c..643f8a9ce 100644 --- a/API/Services/Tasks/Scanner/Parser/DefaultParser.cs +++ b/API/Services/Tasks/Scanner/Parser/DefaultParser.cs @@ -49,6 +49,7 @@ public class DefaultParser : IDefaultParser // If library type is Image or this is not a cover image in a non-image library, then use dedicated parsing mechanism if (type == LibraryType.Image || Parser.IsImage(filePath)) { + // TODO: We can move this up one level return ParseImage(filePath, rootPath, ret); } @@ -83,7 +84,7 @@ public class DefaultParser : IDefaultParser var edition = Parser.ParseEdition(fileName); if (!string.IsNullOrEmpty(edition)) { - ret.Series = Parser.CleanTitle(ret.Series.Replace(edition, ""), type is LibraryType.Comic); + ret.Series = Parser.CleanTitle(ret.Series.Replace(edition, string.Empty), type is LibraryType.Comic); ret.Edition = edition; } diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index 214c2009c..4d49252d5 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -353,7 +353,7 @@ public static class Parser { // Historys Strongest Disciple Kenichi_v11_c90-98.zip, ...c90.5-100.5 new Regex( - @"(\b|_)(c|ch)(\.?\s?)(?(\d+(\.\d)?)-?(\d+(\.\d)?)?)", + @"(\b|_)(c|ch)(\.?\s?)(?(\d+(\.\d)?)(-c?\d+(\.\d)?)?)", MatchOptions, RegexTimeout), // [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip new Regex( @@ -820,6 +820,11 @@ public static class Parser var from = RemoveLeadingZeroes(tokens[0]); if (tokens.Length != 2) return from; + // Occasionally users will use c01-c02 instead of c01-02, clean any leftover c + if (tokens[1].StartsWith("c", StringComparison.InvariantCultureIgnoreCase)) + { + tokens[1] = tokens[1].Replace("c", string.Empty, StringComparison.InvariantCultureIgnoreCase); + } var to = RemoveLeadingZeroes(hasPart ? AddChapterPart(tokens[1]) : tokens[1]); return $"{from}-{to}"; } diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/API/Services/Tasks/Scanner/ProcessSeries.cs index 03a65ace0..6500c88d8 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/API/Services/Tasks/Scanner/ProcessSeries.cs @@ -13,6 +13,7 @@ using API.Entities.Enums; using API.Extensions; using API.Helpers; using API.Helpers.Builders; +using API.Services.Plus; using API.Services.Tasks.Metadata; using API.Services.Tasks.Scanner.Parser; using API.SignalR; @@ -57,6 +58,7 @@ public class ProcessSeries : IProcessSeries private readonly IWordCountAnalyzerService _wordCountAnalyzerService; private readonly ICollectionTagService _collectionTagService; private readonly IReadingListService _readingListService; + private readonly IExternalMetadataService _externalMetadataService; private Dictionary _genres; private IList _people; @@ -66,7 +68,7 @@ public class ProcessSeries : IProcessSeries public ProcessSeries(IUnitOfWork unitOfWork, ILogger logger, IEventHub eventHub, IDirectoryService directoryService, ICacheHelper cacheHelper, IReadingItemService readingItemService, IFileService fileService, IMetadataService metadataService, IWordCountAnalyzerService wordCountAnalyzerService, - ICollectionTagService collectionTagService, IReadingListService readingListService) + ICollectionTagService collectionTagService, IReadingListService readingListService, IExternalMetadataService externalMetadataService) { _unitOfWork = unitOfWork; _logger = logger; @@ -79,6 +81,7 @@ public class ProcessSeries : IProcessSeries _wordCountAnalyzerService = wordCountAnalyzerService; _collectionTagService = collectionTagService; _readingListService = readingListService; + _externalMetadataService = externalMetadataService; _genres = new Dictionary(); @@ -122,11 +125,29 @@ public class ProcessSeries : IProcessSeries } catch (Exception ex) { - // TODO: Output more information to the user - _logger.LogError(ex, "There was an exception finding existing series for {SeriesName} with Localized name of {LocalizedName} for library {LibraryId}. This indicates you have duplicate series with same name or localized name in the library. Correct this and rescan", firstInfo.Series, firstInfo.LocalizedSeries, library.Id); - await _eventHub.SendMessageAsync(MessageFactory.Error, - MessageFactory.ErrorEvent($"There was an exception finding existing series for {firstInfo.Series} with Localized name of {firstInfo.LocalizedSeries} for library {library.Id}", - "This indicates you have duplicate series with same name or localized name in the library. Correct this and rescan.")); + var seriesCollisions = await _unitOfWork.SeriesRepository.GetAllSeriesByAnyName(firstInfo.LocalizedSeries, string.Empty, library.Id, firstInfo.Format); + + seriesCollisions = seriesCollisions.Where(collision => + collision.Name != firstInfo.Series || collision.LocalizedName != firstInfo.LocalizedSeries).ToList(); + + if (seriesCollisions.Count > 1) + { + var firstCollision = seriesCollisions[0]; + var secondCollision = seriesCollisions[1]; + + var tableRows = $"Name: {firstCollision.Name}Name: {secondCollision.Name}" + + $"Localized: {firstCollision.LocalizedName}Localized: {secondCollision.LocalizedName}" + + $"Filename: {Parser.Parser.NormalizePath(firstCollision.FolderPath)}Filename: {Parser.Parser.NormalizePath(secondCollision.FolderPath)}"; + + var htmlTable = $"{string.Join(string.Empty, tableRows)}
Series 1Series 2
"; + + _logger.LogError(ex, "Scanner found a Series {SeriesName} which matched another Series {LocalizedName} in a different folder parallel to Library {LibraryName} root folder. This is not allowed. Please correct", + firstInfo.Series, firstInfo.LocalizedSeries, library.Name); + + await _eventHub.SendMessageAsync(MessageFactory.Error, + MessageFactory.ErrorEvent($"Library {library.Name} Series collision on {firstInfo.Series}", + htmlTable)); + } return; } @@ -218,6 +239,10 @@ public class ProcessSeries : IProcessSeries if (seriesAdded) { + // See if any recommendations can link up to the series and pre-fetch external metadata for the series + _logger.LogInformation("Linking up External Recommendations new series (if applicable)"); + await _externalMetadataService.GetNewSeriesData(series.Id, series.Library.Type); + await _unitOfWork.ExternalSeriesMetadataRepository.LinkRecommendationsToSeries(series); await _eventHub.SendMessageAsync(MessageFactory.SeriesAdded, MessageFactory.SeriesAddedEvent(series.Id, series.Name, series.LibraryId), false); } @@ -732,6 +757,8 @@ public class ProcessSeries : IProcessSeries .Where(s => !string.IsNullOrEmpty(s)) .Select(s => s.Trim()) ); + + // TODO: For each weblink, try to parse out some MetadataIds and store in the Chapter directly for matching (CBL) } if (!string.IsNullOrEmpty(comicInfo.Isbn)) diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index 48cec71ff..c934deb10 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -152,7 +152,9 @@ public class ScannerService : IScannerService _logger.LogCritical("[ScannerService] Multiple series map to this folder. Library scan will be used for ScanFolder"); } } - if (series != null && series.Library.Type != LibraryType.Book) + + // TODO: Figure out why we have the library type restriction here + if (series != null && (series.Library.Type != LibraryType.Book || series.Library.Type != LibraryType.LightNovel)) { if (TaskScheduler.HasScanTaskRunningForSeries(series.Id)) { diff --git a/API/Services/Tasks/StatsService.cs b/API/Services/Tasks/StatsService.cs index affe1300a..ce45cdb28 100644 --- a/API/Services/Tasks/StatsService.cs +++ b/API/Services/Tasks/StatsService.cs @@ -283,7 +283,7 @@ public class StatsService : IStatsService .AsNoTracking() .AsSplitQuery() .MaxAsync(s => s.Volumes! - .Where(v => v.Number == 0) + .Where(v => v.MinNumber == 0) .SelectMany(v => v.Chapters!) .Count()); } diff --git a/API/Services/Tasks/VersionUpdaterService.cs b/API/Services/Tasks/VersionUpdaterService.cs index 5dbcd1d15..200851d10 100644 --- a/API/Services/Tasks/VersionUpdaterService.cs +++ b/API/Services/Tasks/VersionUpdaterService.cs @@ -5,11 +5,9 @@ using System.Threading.Tasks; using API.DTOs.Update; using API.SignalR; using Flurl.Http; -using HtmlAgilityPack; using Kavita.Common.EnvironmentInfo; using Kavita.Common.Helpers; using MarkdownDeep; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace API.Services.Tasks; @@ -47,7 +45,8 @@ public interface IVersionUpdaterService { Task CheckForUpdate(); Task PushUpdate(UpdateNotificationDto update); - Task> GetAllReleases(); + Task> GetAllReleases(); + Task GetNumberOfReleasesBehind(); } public class VersionUpdaterService : IVersionUpdaterService @@ -81,10 +80,27 @@ public class VersionUpdaterService : IVersionUpdaterService return CreateDto(update); } - public async Task> GetAllReleases() + public async Task> GetAllReleases() { var updates = await GetGithubReleases(); - return updates.Select(CreateDto).Where(d => d != null)!; + var updateDtos = updates.Select(CreateDto) + .Where(d => d != null) + .OrderByDescending(d => d!.PublishDate) + .Select(d => d!) + .ToList(); + + // Find the latest dto + var latestRelease = updateDtos[0]!; + var isNightly = BuildInfo.Version > new Version(latestRelease.UpdateVersion); + latestRelease.IsOnNightlyInRelease = isNightly; + + return updateDtos; + } + + public async Task GetNumberOfReleasesBehind() + { + var updates = await GetAllReleases(); + return updates.TakeWhile(update => update.UpdateVersion != update.CurrentVersion).Count(); } private UpdateNotificationDto? CreateDto(GithubReleaseMetadata? update) @@ -101,7 +117,9 @@ public class VersionUpdaterService : IVersionUpdaterService UpdateTitle = update.Name, UpdateUrl = update.Html_Url, IsDocker = OsInfo.IsDocker, - PublishDate = update.Published_At + PublishDate = update.Published_At, + IsReleaseEqual = BuildInfo.Version == updateVersion, + IsReleaseNewer = BuildInfo.Version < updateVersion, }; } @@ -110,7 +128,7 @@ public class VersionUpdaterService : IVersionUpdaterService { if (update == null) return; - var updateVersion = new Version(update.CurrentVersion); + var updateVersion = new Version(update.UpdateVersion); if (BuildInfo.Version < updateVersion) { @@ -118,12 +136,6 @@ public class VersionUpdaterService : IVersionUpdaterService await _eventHub.SendMessageAsync(MessageFactory.UpdateAvailable, MessageFactory.UpdateVersionEvent(update), true); } - else if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == Environments.Development) - { - _logger.LogWarning("Server is up to date. Current: {CurrentVersion}", BuildInfo.Version); - await _eventHub.SendMessageAsync(MessageFactory.UpdateAvailable, MessageFactory.UpdateVersionEvent(update), - true); - } } diff --git a/API/Services/TokenService.cs b/API/Services/TokenService.cs index bc69c0ff2..069b28403 100644 --- a/API/Services/TokenService.cs +++ b/API/Services/TokenService.cs @@ -142,8 +142,9 @@ public class TokenService : ITokenService return jwtClaim?.Value; } - public bool HasTokenExpired(string token) + public bool HasTokenExpired(string? token) { + if (string.IsNullOrEmpty(token)) return true; var tokenHandler = new JwtSecurityTokenHandler(); var tokenContent = tokenHandler.ReadJwtToken(token); return tokenContent.ValidTo <= DateTime.UtcNow; diff --git a/API/SignalR/MessageFactory.cs b/API/SignalR/MessageFactory.cs index 76fcae5fc..44767bd8a 100644 --- a/API/SignalR/MessageFactory.cs +++ b/API/SignalR/MessageFactory.cs @@ -376,13 +376,13 @@ public static class MessageFactory }; } - public static SignalRMessage DownloadProgressEvent(string username, string downloadName, float progress, string eventType = "updated") + public static SignalRMessage DownloadProgressEvent(string username, string downloadName, string subtitle, float progress, string eventType = "updated") { return new SignalRMessage() { Name = DownloadProgress, - Title = $"Downloading {downloadName}", - SubTitle = $"Preparing {username.SentenceCase()} the download of {downloadName}", + Title = $"Preparing {username.SentenceCase()} the download of {downloadName}", + SubTitle = subtitle, EventType = eventType, Progress = ProgressType.Determinate, Body = new diff --git a/API/Startup.cs b/API/Startup.cs index 939bfb586..3b872f396 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -244,12 +244,20 @@ public class Startup await MigrateSmartFilterEncoding.Migrate(unitOfWork, dataContext, logger); await MigrateLibrariesToHaveAllFileTypes.Migrate(unitOfWork, dataContext, logger); + + // v0.7.14 + await MigrateEmailTemplates.Migrate(directoryService, logger); + await MigrateVolumeNumber.Migrate(unitOfWork, dataContext, logger); + await MigrateWantToReadImport.Migrate(unitOfWork, directoryService, logger); + await MigrateManualHistory.Migrate(dataContext, logger); + await MigrateClearNightlyExternalSeriesRecords.Migrate(dataContext, logger); + // Update the version in the DB after all migrations are run var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion); installVersion.Value = BuildInfo.Version.ToString(); unitOfWork.SettingsRepository.Update(installVersion); - await unitOfWork.CommitAsync(); + logger.LogInformation("Running Migrations - complete"); }).GetAwaiter() .GetResult(); @@ -354,11 +362,19 @@ public class Startup context.Response.Headers[HeaderNames.Vary] = new[] { "Accept-Encoding" }; - // Don't let the site be iframed outside the same origin (clickjacking) - context.Response.Headers.XFrameOptions = Configuration.XFrameOptions; - // Setup CSP to ensure we load assets only from these origins - context.Response.Headers.Add("Content-Security-Policy", "frame-ancestors 'none';"); + if (!Configuration.AllowIFraming) + { + // Don't let the site be iframed outside the same origin (clickjacking) + context.Response.Headers.XFrameOptions = "SAMEORIGIN"; + + // Setup CSP to ensure we load assets only from these origins + context.Response.Headers.Add("Content-Security-Policy", "frame-ancestors 'none';"); + } + else + { + logger.LogCritical("appsetting.json has allow iframing on! This may allow for clickjacking on the server. User beware"); + } await next(); }); diff --git a/API/config/appsettings.Development.json b/API/config/appsettings.Development.json index 90faa9e5f..a72749400 100644 --- a/API/config/appsettings.Development.json +++ b/API/config/appsettings.Development.json @@ -2,7 +2,7 @@ "TokenKey": "super secret unguessable key that is longer because we require it", "Port": 5000, "IpAddresses": "", - "BaseUrl": "/test/", + "BaseUrl": "/", "Cache": 90, - "XFrameOrigins": "SAMEORIGIN" + "AllowIFraming": false } \ No newline at end of file diff --git a/API/config/templates/EmailChange.html b/API/config/templates/EmailChange.html new file mode 100644 index 000000000..f5d661294 --- /dev/null +++ b/API/config/templates/EmailChange.html @@ -0,0 +1,348 @@ + + + + + + + + + + + + + Event - [Plain HTML] + + + + + + + + + + + + + + + + + + + + + + +
+ +
Your account's email has been updated on {{InvitingUser}}'s Kavita instance. Click the button to validate your email.
+ + + +
+ + + diff --git a/API/config/templates/EmailConfirm.html b/API/config/templates/EmailConfirm.html new file mode 100644 index 000000000..dff300dc6 --- /dev/null +++ b/API/config/templates/EmailConfirm.html @@ -0,0 +1,348 @@ + + + + + + + + + + + + + Event - [Plain HTML] + + + + + + + + + + + + + + + + + + + + + + +
+ +
You have been invited to {{InvitingUser}}'s Kavita instance. Click the button to accept the invite.
+ + + +
+ + + \ No newline at end of file diff --git a/API/config/templates/EmailMigration.html b/API/config/templates/EmailMigration.html new file mode 100644 index 000000000..f7ea0aed1 --- /dev/null +++ b/API/config/templates/EmailMigration.html @@ -0,0 +1,343 @@ + + + + + + + + + + + + + Kavita - [Plain HTML] + + + + + + + + + + + + + + + + + + + + + + +
+ +
Email confirmation is required for continued access. Click the button to confirm your email.
+ + + +
+ + + \ No newline at end of file diff --git a/API/config/templates/EmailPasswordReset.html b/API/config/templates/EmailPasswordReset.html new file mode 100644 index 000000000..8c7c0a920 --- /dev/null +++ b/API/config/templates/EmailPasswordReset.html @@ -0,0 +1,348 @@ + + + + + + + + + + + + + Kavita - [Plain HTML] + + + + + + + + + + + + + + + + + + + + + + +
+ +
Email confirmation is required for continued access. Click the button to confirm your email.
+ + + +
+ + + \ No newline at end of file diff --git a/API/config/templates/EmailTest.html b/API/config/templates/EmailTest.html new file mode 100644 index 000000000..d408460c4 --- /dev/null +++ b/API/config/templates/EmailTest.html @@ -0,0 +1,325 @@ + + + + + + + + + + + + + Event - [Plain HTML] + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ This is a Test Email +
+ + + +
+ + + diff --git a/API/config/templates/SendToDevice.html b/API/config/templates/SendToDevice.html new file mode 100644 index 000000000..4f82e1975 --- /dev/null +++ b/API/config/templates/SendToDevice.html @@ -0,0 +1,323 @@ + + + + + + + + + + + + + Event - [Plain HTML] + + + + + + + + + + + + + + + + + + + + + + +
+ +
You've been sent a file from Kavita!
+ + + +
+ + + \ No newline at end of file diff --git a/Kavita.Common/Configuration.cs b/Kavita.Common/Configuration.cs index e2af4c32d..ca0fc40ec 100644 --- a/Kavita.Common/Configuration.cs +++ b/Kavita.Common/Configuration.cs @@ -13,7 +13,6 @@ public static class Configuration public const string DefaultBaseUrl = "/"; public const int DefaultHttpPort = 5000; public const int DefaultTimeOutSecs = 90; - public const string DefaultXFrameOptions = "SAMEORIGIN"; public const long DefaultCacheMemory = 75; private static readonly string AppSettingsFilename = Path.Join("config", GetAppSettingFilename()); @@ -49,7 +48,7 @@ public static class Configuration set => SetCacheSize(GetAppSettingFilename(), value); } - public static string XFrameOptions => GetXFrameOptions(GetAppSettingFilename()); + public static bool AllowIFraming => GetAllowIFraming(GetAppSettingFilename()); private static string GetAppSettingFilename() { @@ -293,26 +292,21 @@ public static class Configuration #endregion - #region XFrameOrigins - private static string GetXFrameOptions(string filePath) + #region AllowIFraming + private static bool GetAllowIFraming(string filePath) { - if (OsInfo.IsDocker) - { - return DefaultBaseUrl; - } - try { var json = File.ReadAllText(filePath); var jsonObj = JsonSerializer.Deserialize(json); - return !string.IsNullOrEmpty(jsonObj.XFrameOrigins) ? jsonObj.XFrameOrigins : DefaultXFrameOptions; + return jsonObj.AllowIFraming; } catch (Exception ex) { Console.WriteLine("Error reading app settings: " + ex.Message); } - return DefaultXFrameOptions; + return false; } #endregion @@ -328,6 +322,6 @@ public static class Configuration // ReSharper disable once MemberHidesStaticFromOuterClass public long Cache { get; set; } = DefaultCacheMemory; // ReSharper disable once MemberHidesStaticFromOuterClass - public string XFrameOrigins { get; set; } = DefaultXFrameOptions; + public bool AllowIFraming { get; set; } = false; } } diff --git a/Kavita.Common/EnvironmentInfo/IOsInfo.cs b/Kavita.Common/EnvironmentInfo/IOsInfo.cs index 1f851be00..d8cc6a070 100644 --- a/Kavita.Common/EnvironmentInfo/IOsInfo.cs +++ b/Kavita.Common/EnvironmentInfo/IOsInfo.cs @@ -10,7 +10,9 @@ public static class OsInfo public static bool IsLinux => Os is Os.Linux or Os.LinuxMusl or Os.Bsd; public static bool IsOsx => Os == Os.Osx; public static bool IsWindows => Os == Os.Windows; - public static bool IsDocker => Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true"; + public static bool IsDocker => + Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true" || + Environment.GetEnvironmentVariable("LSIO_FIRST_PARTY") == "true"; static OsInfo() { diff --git a/Kavita.Common/Helpers/CronHelper.cs b/Kavita.Common/Helpers/CronHelper.cs new file mode 100644 index 000000000..77a4e934e --- /dev/null +++ b/Kavita.Common/Helpers/CronHelper.cs @@ -0,0 +1,22 @@ +using System; +using Cronos; + +namespace Kavita.Common.Helpers; + +public static class CronHelper +{ + public static bool IsValidCron(string cronExpression) + { + // NOTE: This must match Hangfire's underlying cron system. Hangfire is unique + try + { + CronExpression.Parse(cronExpression); + return true; + } + catch (Exception ex) + { + /* Swallow */ + return false; + } + } +} diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index 392be3b13..3cba430c8 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -4,19 +4,21 @@ net8.0 kavitareader.com Kavita - 0.7.12.0 + 0.7.14.2 en true + - + all runtime; build; native; contentfiles; analyzers; buildtransitive + - + \ No newline at end of file diff --git a/Kavita.Common/KavitaException.cs b/Kavita.Common/KavitaException.cs index b624e0111..de10d3382 100644 --- a/Kavita.Common/KavitaException.cs +++ b/Kavita.Common/KavitaException.cs @@ -6,7 +6,6 @@ namespace Kavita.Common; /// /// These are used for errors to send to the UI that should not be reported to Sentry /// -[Serializable] public class KavitaException : Exception { public KavitaException() @@ -17,8 +16,4 @@ public class KavitaException : Exception public KavitaException(string message, Exception inner) : base(message, inner) { } - - protected KavitaException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } } diff --git a/Kavita.Common/KavitaUnauthenticatedUserException.cs b/Kavita.Common/KavitaUnauthenticatedUserException.cs index 6cce9f981..ede20b59d 100644 --- a/Kavita.Common/KavitaUnauthenticatedUserException.cs +++ b/Kavita.Common/KavitaUnauthenticatedUserException.cs @@ -7,7 +7,6 @@ namespace Kavita.Common; /// The user does not exist (aka unauthorized). This will be caught by middleware and Unauthorized() returned to UI ///
/// This will always log to Security Log -[Serializable] public class KavitaUnauthenticatedUserException : Exception { public KavitaUnauthenticatedUserException() @@ -18,8 +17,4 @@ public class KavitaUnauthenticatedUserException : Exception public KavitaUnauthenticatedUserException(string message, Exception inner) : base(message, inner) { } - - protected KavitaUnauthenticatedUserException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } } diff --git a/Kavita.Email/DTOs/ConfirmationEmailDto.cs b/Kavita.Email/DTOs/ConfirmationEmailDto.cs new file mode 100644 index 000000000..d157b4d53 --- /dev/null +++ b/Kavita.Email/DTOs/ConfirmationEmailDto.cs @@ -0,0 +1,10 @@ +namespace Skeleton.DTOs; + +public record ConfirmationEmailDto +{ + public string InvitingUser { get; init; } + public string EmailAddress { get; init; } + public string ServerConfirmationLink { get; init; } + public string InstallId { get; init; } + +} \ No newline at end of file diff --git a/Kavita.Email/DTOs/EmailMigrationDto.cs b/Kavita.Email/DTOs/EmailMigrationDto.cs new file mode 100644 index 000000000..dc210dbdb --- /dev/null +++ b/Kavita.Email/DTOs/EmailMigrationDto.cs @@ -0,0 +1,9 @@ +namespace Skeleton.DTOs; + +public class EmailMigrationDto +{ + public string EmailAddress { get; init; } + public string ServerConfirmationLink { get; init; } + public string Username { get; init; } + public string InstallId { get; init; } +} \ No newline at end of file diff --git a/Kavita.Email/DTOs/EmailOptionsDto.cs b/Kavita.Email/DTOs/EmailOptionsDto.cs new file mode 100644 index 000000000..242e618ee --- /dev/null +++ b/Kavita.Email/DTOs/EmailOptionsDto.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace Skeleton.DTOs; + +public class EmailOptionsDto +{ + public IList ToEmails { get; set; } + public string Subject { get; set; } + public string Body { get; set; } + public IList> PlaceHolders { get; set; } + /// + /// Filenames to attach + /// + public IList Attachments { get; set; } +} \ No newline at end of file diff --git a/Kavita.Email/DTOs/PasswordResetDto.cs b/Kavita.Email/DTOs/PasswordResetDto.cs new file mode 100644 index 000000000..901eaa79c --- /dev/null +++ b/Kavita.Email/DTOs/PasswordResetDto.cs @@ -0,0 +1,8 @@ +namespace Skeleton.DTOs; + +public class PasswordResetDto +{ + public string EmailAddress { get; init; } + public string ServerConfirmationLink { get; init; } + public string InstallId { get; init; } +} \ No newline at end of file diff --git a/Kavita.Email/Kavita.Email.csproj b/Kavita.Email/Kavita.Email.csproj new file mode 100644 index 000000000..3a6353295 --- /dev/null +++ b/Kavita.Email/Kavita.Email.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/UI/Web/.gitignore b/UI/Web/.gitignore index dbd64df83..73b400165 100644 --- a/UI/Web/.gitignore +++ b/UI/Web/.gitignore @@ -1,3 +1,4 @@ node_modules/ test-results/ playwright-report/ +i18n-cache-busting.json diff --git a/UI/Web/angular.json b/UI/Web/angular.json index 41eb7a9c1..60fc909a9 100644 --- a/UI/Web/angular.json +++ b/UI/Web/angular.json @@ -68,6 +68,7 @@ "optimization": true, "outputHashing": "all", "namedChunks": false, + "aot": true, "extractLicenses": true, "budgets": [ { diff --git a/UI/Web/hash-localization-prime.js b/UI/Web/hash-localization-prime.js new file mode 100644 index 000000000..013d62b56 --- /dev/null +++ b/UI/Web/hash-localization-prime.js @@ -0,0 +1,3 @@ +const fs = require('fs'); + +fs.writeFileSync('./i18n-cache-busting.json', JSON.stringify({})); diff --git a/UI/Web/hash-localization.js b/UI/Web/hash-localization.js new file mode 100644 index 000000000..547b5af0d --- /dev/null +++ b/UI/Web/hash-localization.js @@ -0,0 +1,35 @@ +const crypto = require('crypto'); +const fs = require('fs'); +const glob = require('glob'); + +const jsonFilesDir = 'dist/browser/assets/langs/'; // Adjust the path to your JSON files +const outputDir = 'dist/browser/assets/langs'; // Directory to store minified files + +function generateChecksum(str, algorithm, encoding) { + return crypto + .createHash(algorithm || 'md5') + .update(str, 'utf8') + .digest(encoding || 'hex'); +} + +const result = {}; + +// Remove file if it exists +const cacheBustingFilePath = './i18n-cache-busting.json'; +if (fs.existsSync(cacheBustingFilePath)) { + console.log('Removing existing file') + fs.unlinkSync(cacheBustingFilePath); +} + +glob.sync(`${jsonFilesDir}**/*.json`).forEach(path => { + let tokens = path.split('dist\\browser\\assets\\langs\\'); + if (tokens.length === 1) { + tokens = path.split('dist/browser/assets/langs/'); + } + const lang = tokens[1]; + const content = fs.readFileSync(path, { encoding: 'utf-8' }); + result[lang.replace('.json', '')] = generateChecksum(content); +}); + +fs.writeFileSync('./i18n-cache-busting.json', JSON.stringify(result)); +fs.writeFileSync(`dist/browser/i18n-cache-busting.json`, JSON.stringify(result)); diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index 4bb7bdb06..fa6b9d010 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -1,24 +1,24 @@ { "name": "kavita-webui", - "version": "0.4.2", + "version": "0.7.12.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kavita-webui", - "version": "0.4.2", + "version": "0.7.12.1", "dependencies": { - "@angular/animations": "^17.0.6", - "@angular/cdk": "^17.0.4", - "@angular/common": "^17.0.6", - "@angular/compiler": "^17.0.6", - "@angular/core": "^17.0.6", - "@angular/forms": "^17.0.6", - "@angular/localize": "^17.0.6", - "@angular/platform-browser": "^17.0.6", - "@angular/platform-browser-dynamic": "^17.0.6", - "@angular/router": "^17.0.6", - "@fortawesome/fontawesome-free": "^6.4.2", + "@angular/animations": "^17.1.0", + "@angular/cdk": "^17.1.0", + "@angular/common": "^17.1.0", + "@angular/compiler": "^17.1.0", + "@angular/core": "^17.1.0", + "@angular/forms": "^17.1.0", + "@angular/localize": "^17.1.0", + "@angular/platform-browser": "^17.1.0", + "@angular/platform-browser-dynamic": "^17.1.0", + "@angular/router": "^17.1.0", + "@fortawesome/fontawesome-free": "^6.5.1", "@iharbeck/ngx-virtual-scroller": "^17.0.0", "@iplab/ngx-file-upload": "^17.0.0", "@microsoft/signalr": "^7.0.12", @@ -37,7 +37,7 @@ "luxon": "^3.4.4", "ng-circle-progress": "^1.7.1", "ng-lazyload-image": "^9.1.3", - "ng-select2-component": "^13.0.9", + "ng-select2-component": "^14.0.0", "ngx-color-picker": "^16.0.0", "ngx-extended-pdf-viewer": "^18.1.9", "ngx-file-drop": "^16.0.0", @@ -53,20 +53,20 @@ "zone.js": "^0.14.2" }, "devDependencies": { - "@angular-devkit/build-angular": "^17.0.7", - "@angular-eslint/builder": "^17.1.1", - "@angular-eslint/eslint-plugin": "^17.1.1", - "@angular-eslint/eslint-plugin-template": "^17.1.1", - "@angular-eslint/schematics": "^17.1.1", - "@angular-eslint/template-parser": "^17.1.1", - "@angular/cli": "^17.0.7", - "@angular/compiler-cli": "^17.0.6", + "@angular-devkit/build-angular": "^17.1.0", + "@angular-eslint/builder": "^17.2.1", + "@angular-eslint/eslint-plugin": "^17.2.1", + "@angular-eslint/eslint-plugin-template": "^17.2.1", + "@angular-eslint/schematics": "^17.2.1", + "@angular-eslint/template-parser": "^17.2.1", + "@angular/cli": "^17.1.0", + "@angular/compiler-cli": "^17.1.0", "@types/d3": "^7.4.3", "@types/file-saver": "^2.0.7", - "@types/luxon": "^3.3.7", + "@types/luxon": "^3.4.0", "@types/node": "^20.10.0", "@typescript-eslint/eslint-plugin": "^6.13.0", - "@typescript-eslint/parser": "^6.13.0", + "@typescript-eslint/parser": "^6.19.0", "eslint": "^8.54.0", "jsonminify": "^0.4.2", "karma-coverage": "~2.2.0", @@ -97,12 +97,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1700.7", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1700.7.tgz", - "integrity": "sha512-32uitQKsYLGXAKoXBsmOnPsTt9pS+b9cnFI9ZvBFVhJ31I2EOM7vGcMFalhTxdB/DkVHk4TyO78efV0V26DwCA==", + "version": "0.1701.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1701.0.tgz", + "integrity": "sha512-VP6mjptKFn0HO2dn4bH0mFMe4CrexlWlgnTHyAUbL7ZFaV9w4VQuE/vXr60wMlQ+83NIGUeJImjt1QVNlIjJnQ==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.0.7", + "@angular-devkit/core": "17.1.0", "rxjs": "7.8.1" }, "engines": { @@ -112,42 +112,40 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "17.0.7", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.0.7.tgz", - "integrity": "sha512-AtEzLk6n6BXqQzk0Bsupe6GV0IgUe7RbpBfqROi+NZqMA7OUAHCX3xA6M68Qu+5KxBtW7T5lHeZZ7iP/y39wtQ==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.1.0.tgz", + "integrity": "sha512-N9B2SlKewD48qKFgRPKDH1X2EvOGll1ocMlFxi95mT9aXuFd2d75JUYHzS1v3FQRU3peoAoFKxCV7OuIL/cmTA==", "dev": true, "dependencies": { "@ampproject/remapping": "2.2.1", - "@angular-devkit/architect": "0.1700.7", - "@angular-devkit/build-webpack": "0.1700.7", - "@angular-devkit/core": "17.0.7", - "@babel/core": "7.23.2", - "@babel/generator": "7.23.0", + "@angular-devkit/architect": "0.1701.0", + "@angular-devkit/build-webpack": "0.1701.0", + "@angular-devkit/core": "17.1.0", + "@babel/core": "7.23.7", + "@babel/generator": "7.23.6", "@babel/helper-annotate-as-pure": "7.22.5", "@babel/helper-split-export-declaration": "7.22.6", - "@babel/plugin-transform-async-generator-functions": "7.23.2", - "@babel/plugin-transform-async-to-generator": "7.22.5", - "@babel/plugin-transform-runtime": "7.23.2", - "@babel/preset-env": "7.23.2", - "@babel/runtime": "7.23.2", + "@babel/plugin-transform-async-generator-functions": "7.23.7", + "@babel/plugin-transform-async-to-generator": "7.23.3", + "@babel/plugin-transform-runtime": "7.23.7", + "@babel/preset-env": "7.23.7", + "@babel/runtime": "7.23.7", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "17.0.7", - "@vitejs/plugin-basic-ssl": "1.0.1", + "@ngtools/webpack": "17.1.0", + "@vitejs/plugin-basic-ssl": "1.0.2", "ansi-colors": "4.1.3", "autoprefixer": "10.4.16", "babel-loader": "9.1.3", "babel-plugin-istanbul": "6.1.1", - "browser-sync": "2.29.3", "browserslist": "^4.21.5", - "chokidar": "3.5.3", "copy-webpack-plugin": "11.0.0", "critters": "0.0.20", "css-loader": "6.8.1", - "esbuild-wasm": "0.19.5", - "fast-glob": "3.3.1", + "esbuild-wasm": "0.19.11", + "fast-glob": "3.3.2", "http-proxy-middleware": "2.0.6", "https-proxy-agent": "7.0.2", - "inquirer": "9.2.11", + "inquirer": "9.2.12", "jsonc-parser": "3.2.0", "karma-source-map-support": "1.4.0", "less": "4.2.0", @@ -156,27 +154,28 @@ "loader-utils": "3.2.1", "magic-string": "0.30.5", "mini-css-extract-plugin": "2.7.6", - "mrmime": "1.0.1", + "mrmime": "2.0.0", "open": "8.4.2", "ora": "5.4.1", "parse5-html-rewriting-stream": "7.0.0", "picomatch": "3.0.1", - "piscina": "4.1.0", - "postcss": "8.4.31", - "postcss-loader": "7.3.3", + "piscina": "4.2.1", + "postcss": "8.4.33", + "postcss-loader": "7.3.4", "resolve-url-loader": "5.0.0", "rxjs": "7.8.1", - "sass": "1.69.5", - "sass-loader": "13.3.2", + "sass": "1.69.7", + "sass-loader": "13.3.3", "semver": "7.5.4", - "source-map-loader": "4.0.1", + "source-map-loader": "5.0.0", "source-map-support": "0.5.21", - "terser": "5.24.0", + "terser": "5.26.0", "text-table": "0.2.0", "tree-kill": "1.2.2", "tslib": "2.6.2", - "undici": "5.27.2", - "vite": "4.5.1", + "undici": "6.2.1", + "vite": "5.0.11", + "watchpack": "2.4.0", "webpack": "5.89.0", "webpack-dev-middleware": "6.1.1", "webpack-dev-server": "4.15.1", @@ -189,20 +188,22 @@ "yarn": ">= 1.13.0" }, "optionalDependencies": { - "esbuild": "0.19.5" + "esbuild": "0.19.11" }, "peerDependencies": { "@angular/compiler-cli": "^17.0.0", "@angular/localize": "^17.0.0", "@angular/platform-server": "^17.0.0", "@angular/service-worker": "^17.0.0", + "@web/test-runner": "^0.18.0", + "browser-sync": "^3.0.2", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", "karma": "^6.3.0", "ng-packagr": "^17.0.0", "protractor": "^7.0.0", "tailwindcss": "^2.0.0 || ^3.0.0", - "typescript": ">=5.2 <5.3" + "typescript": ">=5.2 <5.4" }, "peerDependenciesMeta": { "@angular/localize": { @@ -214,6 +215,12 @@ "@angular/service-worker": { "optional": true }, + "@web/test-runner": { + "optional": true + }, + "browser-sync": { + "optional": true + }, "jest": { "optional": true }, @@ -234,404 +241,72 @@ } } }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/android-arm": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.5.tgz", - "integrity": "sha512-bhvbzWFF3CwMs5tbjf3ObfGqbl/17ict2/uwOSfr3wmxDE6VdS2GqY/FuzIPe0q0bdhj65zQsvqfArI9MY6+AA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/android-arm64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.5.tgz", - "integrity": "sha512-5d1OkoJxnYQfmC+Zd8NBFjkhyCNYwM4n9ODrycTFY6Jk1IGiZ+tjVJDDSwDt77nK+tfpGP4T50iMtVi4dEGzhQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/android-x64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.5.tgz", - "integrity": "sha512-9t+28jHGL7uBdkBjL90QFxe7DVA+KGqWlHCF8ChTKyaKO//VLuoBricQCgwhOjA1/qOczsw843Fy4cbs4H3DVA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/darwin-arm64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.5.tgz", - "integrity": "sha512-mvXGcKqqIqyKoxq26qEDPHJuBYUA5KizJncKOAf9eJQez+L9O+KfvNFu6nl7SCZ/gFb2QPaRqqmG0doSWlgkqw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/darwin-x64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.5.tgz", - "integrity": "sha512-Ly8cn6fGLNet19s0X4unjcniX24I0RqjPv+kurpXabZYSXGM4Pwpmf85WHJN3lAgB8GSth7s5A0r856S+4DyiA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.5.tgz", - "integrity": "sha512-GGDNnPWTmWE+DMchq1W8Sd0mUkL+APvJg3b11klSGUDvRXh70JqLAO56tubmq1s2cgpVCSKYywEiKBfju8JztQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/freebsd-x64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.5.tgz", - "integrity": "sha512-1CCwDHnSSoA0HNwdfoNY0jLfJpd7ygaLAp5EHFos3VWJCRX9DMwWODf96s9TSse39Br7oOTLryRVmBoFwXbuuQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-arm": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.5.tgz", - "integrity": "sha512-lrWXLY/vJBzCPC51QN0HM71uWgIEpGSjSZZADQhq7DKhPcI6NH1IdzjfHkDQws2oNpJKpR13kv7/pFHBbDQDwQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-arm64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.5.tgz", - "integrity": "sha512-o3vYippBmSrjjQUCEEiTZ2l+4yC0pVJD/Dl57WfPwwlvFkrxoSO7rmBZFii6kQB3Wrn/6GwJUPLU5t52eq2meA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-ia32": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.5.tgz", - "integrity": "sha512-MkjHXS03AXAkNp1KKkhSKPOCYztRtK+KXDNkBa6P78F8Bw0ynknCSClO/ztGszILZtyO/lVKpa7MolbBZ6oJtQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-loong64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.5.tgz", - "integrity": "sha512-42GwZMm5oYOD/JHqHska3Jg0r+XFb/fdZRX+WjADm3nLWLcIsN27YKtqxzQmGNJgu0AyXg4HtcSK9HuOk3v1Dw==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-mips64el": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.5.tgz", - "integrity": "sha512-kcjndCSMitUuPJobWCnwQ9lLjiLZUR3QLQmlgaBfMX23UEa7ZOrtufnRds+6WZtIS9HdTXqND4yH8NLoVVIkcg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-ppc64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.5.tgz", - "integrity": "sha512-yJAxJfHVm0ZbsiljbtFFP1BQKLc8kUF6+17tjQ78QjqjAQDnhULWiTA6u0FCDmYT1oOKS9PzZ2z0aBI+Mcyj7Q==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-riscv64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.5.tgz", - "integrity": "sha512-5u8cIR/t3gaD6ad3wNt1MNRstAZO+aNyBxu2We8X31bA8XUNyamTVQwLDA1SLoPCUehNCymhBhK3Qim1433Zag==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-s390x": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.5.tgz", - "integrity": "sha512-Z6JrMyEw/EmZBD/OFEFpb+gao9xJ59ATsoTNlj39jVBbXqoZm4Xntu6wVmGPB/OATi1uk/DB+yeDPv2E8PqZGw==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-x64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.5.tgz", - "integrity": "sha512-psagl+2RlK1z8zWZOmVdImisMtrUxvwereIdyJTmtmHahJTKb64pAcqoPlx6CewPdvGvUKe2Jw+0Z/0qhSbG1A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/netbsd-x64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.5.tgz", - "integrity": "sha512-kL2l+xScnAy/E/3119OggX8SrWyBEcqAh8aOY1gr4gPvw76la2GlD4Ymf832UCVbmuWeTf2adkZDK+h0Z/fB4g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/openbsd-x64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.5.tgz", - "integrity": "sha512-sPOfhtzFufQfTBgRnE1DIJjzsXukKSvZxloZbkJDG383q0awVAq600pc1nfqBcl0ice/WN9p4qLc39WhBShRTA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/sunos-x64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.5.tgz", - "integrity": "sha512-dGZkBXaafuKLpDSjKcB0ax0FL36YXCvJNnztjKV+6CO82tTYVDSH2lifitJ29jxRMoUhgkg9a+VA/B03WK5lcg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/win32-arm64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.5.tgz", - "integrity": "sha512-dWVjD9y03ilhdRQ6Xig1NWNgfLtf2o/STKTS+eZuF90fI2BhbwD6WlaiCGKptlqXlURVB5AUOxUj09LuwKGDTg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/win32-ia32": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.5.tgz", - "integrity": "sha512-4liggWIA4oDgUxqpZwrDhmEfAH4d0iljanDOK7AnVU89T6CzHon/ony8C5LeOdfgx60x5cnQJFZwEydVlYx4iw==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/win32-x64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.5.tgz", - "integrity": "sha512-czTrygUsB/jlM8qEW5MD8bgYU2Xg14lo6kBDXW6HdxKjh8M5PzETGiSHaz9MtbXBYDloHNUAUW2tMiKW4KM9Mw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/esbuild": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.5.tgz", - "integrity": "sha512-bUxalY7b1g8vNhQKdB24QDmHeY4V4tw/s6Ak5z+jJX9laP5MoQseTOMemAr0gxssjNcH0MCViG8ONI2kksvfFQ==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.19.5", - "@esbuild/android-arm64": "0.19.5", - "@esbuild/android-x64": "0.19.5", - "@esbuild/darwin-arm64": "0.19.5", - "@esbuild/darwin-x64": "0.19.5", - "@esbuild/freebsd-arm64": "0.19.5", - "@esbuild/freebsd-x64": "0.19.5", - "@esbuild/linux-arm": "0.19.5", - "@esbuild/linux-arm64": "0.19.5", - "@esbuild/linux-ia32": "0.19.5", - "@esbuild/linux-loong64": "0.19.5", - "@esbuild/linux-mips64el": "0.19.5", - "@esbuild/linux-ppc64": "0.19.5", - "@esbuild/linux-riscv64": "0.19.5", - "@esbuild/linux-s390x": "0.19.5", - "@esbuild/linux-x64": "0.19.5", - "@esbuild/netbsd-x64": "0.19.5", - "@esbuild/openbsd-x64": "0.19.5", - "@esbuild/sunos-x64": "0.19.5", - "@esbuild/win32-arm64": "0.19.5", - "@esbuild/win32-ia32": "0.19.5", - "@esbuild/win32-x64": "0.19.5" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/@angular-devkit/build-angular/node_modules/@babel/core": { + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.7.tgz", + "integrity": "sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==", "dev": true, "dependencies": { - "yallist": "^4.0.0" + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.7", + "@babel/parser": "^7.23.6", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, "engines": { "node": ">=10" } @@ -648,34 +323,13 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/@angular-devkit/build-angular/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1700.7", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1700.7.tgz", - "integrity": "sha512-B9Mg/qYDpE5my8PJ3VPQyRSUV0Oq1bFUzU8s0ZpqEZl1URKc04pm0LtLmebrMIcUZgDiGk0RHaD+O1E9IV/bdQ==", + "version": "0.1701.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1701.0.tgz", + "integrity": "sha512-AUQbdnAXMdXKPj51RWr+0SusTh5M1EWEpXtEZgDSO5Vab6ak+xsX+k1IhjlEoliF0prHjD5WzBegr6WKCjZ30w==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1700.7", + "@angular-devkit/architect": "0.1701.0", "rxjs": "7.8.1" }, "engines": { @@ -689,9 +343,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "17.0.7", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.7.tgz", - "integrity": "sha512-vATobHo5O5tJba424hJfQWLb40GzvZPNsI74dcgSUTgrDph8ksmk5xB9OvEvf0INorQZ2IMphj/VIWj4/+JqSA==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.1.0.tgz", + "integrity": "sha512-w7HeJjyM6YtjXrwFdmFIsp9lzDPAFJov8hVCD18DZaCwryRixz+o8egfw2SkpI4L8kuGAiGxpaCTRsTQtmR4/w==", "dev": true, "dependencies": { "ajv": "8.12.0", @@ -728,12 +382,12 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "17.0.7", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.0.7.tgz", - "integrity": "sha512-BY11OkJkM3xyXcvyD7x5kGY/c8Ufd4AfPvI0D9imhVxbns45Q48b1DlvCQvSnCJ/s+OwnkrYb/Efa70ZiaGu8A==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.1.0.tgz", + "integrity": "sha512-7q4Bk3+ePBdzrmMWxWBnNdN4kmBe2jJwa3vAofaMqZiIBEor85YcOsrUJvcWM/3+/TusgZr4p/4+oJgiYDrj5A==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.0.7", + "@angular-devkit/core": "17.1.0", "jsonc-parser": "3.2.0", "magic-string": "0.30.5", "ora": "5.4.1", @@ -746,13 +400,13 @@ } }, "node_modules/@angular-eslint/builder": { - "version": "17.1.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-17.1.1.tgz", - "integrity": "sha512-QGnIaypNP1osDObTIRJ5JF1KdMBn2oghZXMZAFN+qc+4+EX0SLfrSVw0YTZRH1Sg8ns3/Q+E6jYrswrhV1JmKQ==", + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-17.2.1.tgz", + "integrity": "sha512-O30eaR0wCPiP+zKWvXj2JM8hVq30Wok2rp7zJMFm3PurjF9nWIIyexXkE5fa538DYZYxu8N3gQRqhpv5jvTXCg==", "dev": true, "dependencies": { - "@nx/devkit": "17.1.3", - "nx": "17.1.3" + "@nx/devkit": "17.2.8", + "nx": "17.2.8" }, "peerDependencies": { "eslint": "^7.20.0 || ^8.0.0", @@ -760,19 +414,19 @@ } }, "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "17.1.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-17.1.1.tgz", - "integrity": "sha512-xRlSh9qjdUdUKAy/0UQsxX7wf1tHApAsHsfismebPriqfmVAPyEg4HBrM8ImWaZxiqaTGC1AyHsUBQD5FK8o6w==", + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-17.2.1.tgz", + "integrity": "sha512-puC0itsZv2QlrDOCcWtq1KZH+DvfrpV+mV78HHhi6+h25R5iIhr8ARKcl3EQxFjvrFq34jhG8pSupxKvFbHVfA==", "dev": true }, "node_modules/@angular-eslint/eslint-plugin": { - "version": "17.1.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-17.1.1.tgz", - "integrity": "sha512-fFOBlCOVObVu3gjLj+0BypqO1ZR/0bfJnDElqMdYwJG7zRaFT8NNQbrOo/q/GQoqOFoNna6mw3teTGsd5JnL2A==", + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-17.2.1.tgz", + "integrity": "sha512-9yA81BHpsaCUKRBtHGN3ieAy8HpIoffzPQMu34lYqZFT4yGHGhYmhQjNSQGBRbV2LD9dVv2U35rMHNmUcozXpw==", "dev": true, "dependencies": { - "@angular-eslint/utils": "17.1.1", - "@typescript-eslint/utils": "6.13.1" + "@angular-eslint/utils": "17.2.1", + "@typescript-eslint/utils": "6.19.0" }, "peerDependencies": { "eslint": "^7.20.0 || ^8.0.0", @@ -780,15 +434,15 @@ } }, "node_modules/@angular-eslint/eslint-plugin-template": { - "version": "17.1.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-17.1.1.tgz", - "integrity": "sha512-unZ6QNwtxuB8Eni7UPdw7uK6iZipZUXIsH+ZuLMOxwFgGMqeRnpv8SW0212rto3d/Ec0jESzVHKcwZ9pT+jxgw==", + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-17.2.1.tgz", + "integrity": "sha512-hl1hcHtcm90wyVL1OQGTz16oA0KHon+FFb3Qg0fLXObaXxA495Ecefd9ub5Xxg4JEOPRDi29bF1Y3YKpwflgeg==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "17.1.1", - "@angular-eslint/utils": "17.1.1", - "@typescript-eslint/type-utils": "6.13.1", - "@typescript-eslint/utils": "6.13.1", + "@angular-eslint/bundled-angular-compiler": "17.2.1", + "@angular-eslint/utils": "17.2.1", + "@typescript-eslint/type-utils": "6.19.0", + "@typescript-eslint/utils": "6.19.0", "aria-query": "5.3.0", "axobject-query": "4.0.0" }, @@ -798,16 +452,16 @@ } }, "node_modules/@angular-eslint/schematics": { - "version": "17.1.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-17.1.1.tgz", - "integrity": "sha512-Bkt8iOXWRQGSrcLRGzdyJLvSPcIChW5+dh5lXa5GhdLmVAF7jpjxqGwW0rNb5JhLa/phyH0XQIpLBaOPtacSMA==", + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-17.2.1.tgz", + "integrity": "sha512-7ldtIePI4ZTp/TBpeOZkzfv30HSAn//4TgtFuqvojudI8n8batV5FqQ0VNm1e0zitl75t8Zwtr0KYT4I6vh59g==", "dev": true, "dependencies": { - "@angular-eslint/eslint-plugin": "17.1.1", - "@angular-eslint/eslint-plugin-template": "17.1.1", - "@nx/devkit": "17.1.3", + "@angular-eslint/eslint-plugin": "17.2.1", + "@angular-eslint/eslint-plugin-template": "17.2.1", + "@nx/devkit": "17.2.8", "ignore": "5.3.0", - "nx": "17.1.3", + "nx": "17.2.8", "strip-json-comments": "3.1.1", "tmp": "0.2.1" }, @@ -816,27 +470,43 @@ } }, "node_modules/@angular-eslint/template-parser": { - "version": "17.1.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-17.1.1.tgz", - "integrity": "sha512-ofL46rNhRVeSxrSQF0vwhKMco+vJuo+ZGjSOzFmT9N3KAMB0j+WXTbpyGGMy0gQSBc4W6p+j+zxGa2CR2xb6wA==", + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-17.2.1.tgz", + "integrity": "sha512-WPQYFvRju0tCDXQ/pwrzC911pE07JvpeDgcN2elhzV6lxDHJEZpA5O9pnW9qgNA6J6XM9Q7dBkJ22ztAzC4WFw==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "17.1.1", - "eslint-scope": "^7.0.0" + "@angular-eslint/bundled-angular-compiler": "17.2.1", + "eslint-scope": "^8.0.0" }, "peerDependencies": { "eslint": "^7.20.0 || ^8.0.0", "typescript": "*" } }, - "node_modules/@angular-eslint/utils": { - "version": "17.1.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-17.1.1.tgz", - "integrity": "sha512-CTNPOb05S/DII/Fm8JYUvKo+B4u/ctHjGJ0X1YXUR0q31oaGqTE3KePGq76+Y6swRDf9NjUIcfcnZp3u3j4CBQ==", + "node_modules/@angular-eslint/template-parser/node_modules/eslint-scope": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.0.tgz", + "integrity": "sha512-zj3Byw6jX4TcFCJmxOzLt6iol5FAr9xQyZZSQjEzW2UiCJXLwXdRIKCYVFftnpZckaC9Ps9xlC7jB8tSeWWOaw==", "dev": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "17.1.1", - "@typescript-eslint/utils": "6.13.1" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@angular-eslint/utils": { + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-17.2.1.tgz", + "integrity": "sha512-qQYTBXy90dWM7fhhpa5i9lTtqqhJisvRa+naCrQx9kBgR458JScLdkVIdcZ9D/rPiDCmKiVUfgcDISnjUeqTqg==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "17.2.1", + "@typescript-eslint/utils": "6.19.0" }, "peerDependencies": { "eslint": "^7.20.0 || ^8.0.0", @@ -844,9 +514,9 @@ } }, "node_modules/@angular/animations": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.0.6.tgz", - "integrity": "sha512-fic61LjLHry79c5H9UGM8Ff311MJnf9an7EukLj2aLJ3J0uadL/H9de7dDp8PaIT10DX9g+aRTIKOmF3PmmXIQ==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.1.0.tgz", + "integrity": "sha512-EzyJsla/CnRX4ARmHe9J1m3Pl+J4m5hznzeQFyZpJehikaHKAGGJTGM/+DFAX9TuR1ZpCmS0z0oWsYzag2Q7RA==", "dependencies": { "tslib": "^2.3.0" }, @@ -854,13 +524,13 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/core": "17.0.6" + "@angular/core": "17.1.0" } }, "node_modules/@angular/cdk": { - "version": "17.0.4", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.0.4.tgz", - "integrity": "sha512-mh/EuIR0NPfpNqAXBSZWuJeBMXUvUDYdKhiFWZet5NLO1bDgFe1MGLBjtW4us95k4BZsMLbCKNxJgc+4JqwUvg==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.1.0.tgz", + "integrity": "sha512-a2+uqr1s2pCStFs78BM1ViVqi0GnxFHGKHo58hiR9pDV/pyg9cvy+d+rsci1HkuF9AC/UqV5Y6rGLfwayO183g==", "dependencies": { "tslib": "^2.3.0" }, @@ -874,25 +544,25 @@ } }, "node_modules/@angular/cli": { - "version": "17.0.7", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.0.7.tgz", - "integrity": "sha512-oSa0GVAQNA7wFbLJYeaO3kV4iUcbKEqXDLxcIE8s1GfHddBOlXH2P1T4fXonCBl5qvV+joP0G0+fs7I0w2utZQ==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.1.0.tgz", + "integrity": "sha512-mZh8ibV94CqHls+GTHok9rF78UvrtKZx+o1QOcG50ZM1L5O5s2NYrBhf+QXVeTTmzhSH1wXQb7ueyuLNLVB/eA==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1700.7", - "@angular-devkit/core": "17.0.7", - "@angular-devkit/schematics": "17.0.7", - "@schematics/angular": "17.0.7", + "@angular-devkit/architect": "0.1701.0", + "@angular-devkit/core": "17.1.0", + "@angular-devkit/schematics": "17.1.0", + "@schematics/angular": "17.1.0", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "ini": "4.1.1", - "inquirer": "9.2.11", + "inquirer": "9.2.12", "jsonc-parser": "3.2.0", "npm-package-arg": "11.0.1", "npm-pick-manifest": "9.0.0", "open": "8.4.2", "ora": "5.4.1", - "pacote": "17.0.4", + "pacote": "17.0.5", "resolve": "1.22.8", "semver": "7.5.4", "symbol-observable": "4.0.0", @@ -907,43 +577,10 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@angular/cli/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@angular/cli/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@angular/cli/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@angular/common": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.0.6.tgz", - "integrity": "sha512-FZtf8ol8W2V21ZDgFtcxmJ6JJKUO97QZ+wr/bosyYEryWMmn6VGrbOARhfW7BlrEgn14NdFkLb72KKtqoqRjrg==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.1.0.tgz", + "integrity": "sha512-0Zg62iSynyRr2QslC8dVwSo46mkKrVENnwcBvsgTJ8rfGiuRdKMX8nWm5EUEm3ohKmYLfHvyEjsKDRn//UefVw==", "dependencies": { "tslib": "^2.3.0" }, @@ -951,14 +588,14 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/core": "17.0.6", + "@angular/core": "17.1.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.0.6.tgz", - "integrity": "sha512-PaCNnlPcL0rvByKCBUUyLWkKJYXOrcfKlYYvcacjOzEUgZeEpekG81hMRb9u/Pz+A+M4HJSTmdgzwGP35zo8qw==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.1.0.tgz", + "integrity": "sha512-gF4i/WtPSiSvT4YNasTNnckOxdxuSNwi0EsncrtewwveBcCatjqaXNssUCiF5TgxlC2sKTmsPcMqDJrfX2LMpw==", "dependencies": { "tslib": "^2.3.0" }, @@ -966,7 +603,7 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/core": "17.0.6" + "@angular/core": "17.1.0" }, "peerDependenciesMeta": { "@angular/core": { @@ -975,9 +612,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.0.6.tgz", - "integrity": "sha512-C1Gfh9kbjYZezEMOwxnvUTHuPXa+6pk7mAfSj8e5oAO6E+wfo2dTxv1J5zxa3KYzxPYMNfF8OFvLuMKsw7lXjA==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.1.0.tgz", + "integrity": "sha512-WDpO4WvC5ItjaRexnpFpKPpT+cu+5GYkWF8h74iHhfxOgU+gaQiMWERHylWCqF25AzmhKu0iI3ZZtaIJ6qqwog==", "dev": true, "dependencies": { "@babel/core": "7.23.2", @@ -998,14 +635,14 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/compiler": "17.0.6", - "typescript": ">=5.2 <5.3" + "@angular/compiler": "17.1.0", + "typescript": ">=5.2 <5.4" } }, "node_modules/@angular/core": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.0.6.tgz", - "integrity": "sha512-QzfKRTDNgGOY9D5VxenUUz20cvPVC+uVw9xiqkDuHgGfLYVFlCAK9ymFYkdUCLTcVzJPxckP+spMpPX8nc4Aqw==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.1.0.tgz", + "integrity": "sha512-9OvRRZq+46S+ICZLRYIGVU2pknuPz23B+5V3jz7cDA5V43GVcMnfmAbMClPQxm7kRGnqtQ+yzBjn+HubCerE6g==", "dependencies": { "tslib": "^2.3.0" }, @@ -1018,9 +655,9 @@ } }, "node_modules/@angular/forms": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.0.6.tgz", - "integrity": "sha512-n/trsMtQHUBGiWz5lFaggMcMOuw0gH+96TCtHxQiUYJOdrbOemkFdGrNh3B4fGHmogWuOYJVF5FAm97WRES2XA==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.1.0.tgz", + "integrity": "sha512-JD9IAxa5gQnjzxYJXm3H+lBuyv/dCnPHl6fpvb/JGrxY6xi4gfndyI8AkAb/wOAQgZDsIPaq5s4eWDjhr7CpyA==", "dependencies": { "tslib": "^2.3.0" }, @@ -1028,18 +665,19 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "17.0.6", - "@angular/core": "17.0.6", - "@angular/platform-browser": "17.0.6", + "@angular/common": "17.1.0", + "@angular/core": "17.1.0", + "@angular/platform-browser": "17.1.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/localize": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-17.0.6.tgz", - "integrity": "sha512-LHLadqRQ4lnM0VxWZld2JLO50t3URi7on0YhZp6p/xhY6cQLcTG3GDNGC/k8dg8JW/7aXJU0hmJEMcop9ELM7g==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-17.1.0.tgz", + "integrity": "sha512-GW+1F72lRnCwppu2GzGP04d3UhtdhqMHlCbBdZzQUbv8XQfU+22MOGZx/Ry8sXnanZDgH+u+2A4bvrKZPsVgZg==", "dependencies": { "@babel/core": "7.23.2", + "@types/babel__core": "7.20.2", "fast-glob": "3.3.1", "yargs": "^17.2.1" }, @@ -1052,14 +690,14 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/compiler": "17.0.6", - "@angular/compiler-cli": "17.0.6" + "@angular/compiler": "17.1.0", + "@angular/compiler-cli": "17.1.0" } }, "node_modules/@angular/platform-browser": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.0.6.tgz", - "integrity": "sha512-nBhWH1MKT2WswgRNIoMnmNAt0n5/fG59BanJtodW71//Aj5aIE+BuVoFgK3wmO8IMoeP4i4GXRInBXs6lUMOJw==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.1.0.tgz", + "integrity": "sha512-Klq92ZUX0+ZsxLvbYtIEP3GtVEfMLYPxmBP0pWNZyYIeJCg/YxPS76QSvEhBaMqFelk4RzkDQEIfixC16UIgOA==", "dependencies": { "tslib": "^2.3.0" }, @@ -1067,9 +705,9 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/animations": "17.0.6", - "@angular/common": "17.0.6", - "@angular/core": "17.0.6" + "@angular/animations": "17.1.0", + "@angular/common": "17.1.0", + "@angular/core": "17.1.0" }, "peerDependenciesMeta": { "@angular/animations": { @@ -1078,9 +716,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.0.6.tgz", - "integrity": "sha512-5ZEmBtBkqamTaWjUXCls7G1f3xyK/ykXE7hnUV9CgGqXKrNkxblmbtOhoWdsbuIYjjdxQcAk1qtg/Rg21wcc4w==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.1.0.tgz", + "integrity": "sha512-rqPRZZx6VcSx81HIQr1XMBgb7fYSj6pOZNTJGZkn2KNxrz6hyU3A3qaom1VSVRK5vvNb1cFn35mg/zyOIliTIg==", "dependencies": { "tslib": "^2.3.0" }, @@ -1088,16 +726,16 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "17.0.6", - "@angular/compiler": "17.0.6", - "@angular/core": "17.0.6", - "@angular/platform-browser": "17.0.6" + "@angular/common": "17.1.0", + "@angular/compiler": "17.1.0", + "@angular/core": "17.1.0", + "@angular/platform-browser": "17.1.0" } }, "node_modules/@angular/router": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.0.6.tgz", - "integrity": "sha512-xW6yDxREpBOB9MoODSfIw5HwkwLK+OgK34Q6sGYs0ft9UryMoFwft+pHGAaDz2nzhA72n+Ht9B2eai78UE9jGQ==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.1.0.tgz", + "integrity": "sha512-VDeVLiiS4iEwqwgsLyL9hqA1djFW3yveMnhZIwviJlnp9vG2r/ggMKhNmdP1Hb2iaNgflyhyhwafJ0gi9SLi5A==", "dependencies": { "tslib": "^2.3.0" }, @@ -1105,9 +743,9 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "17.0.6", - "@angular/core": "17.0.6", - "@angular/platform-browser": "17.0.6", + "@angular/common": "17.1.0", + "@angular/core": "17.1.0", + "@angular/platform-browser": "17.1.0", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -1118,11 +756,11 @@ "dev": true }, "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dependencies": { - "@babel/highlight": "^7.22.13", + "@babel/highlight": "^7.23.4", "chalk": "^2.4.2" }, "engines": { @@ -1130,9 +768,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.2.tgz", - "integrity": "sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", "engines": { "node": ">=6.9.0" } @@ -1180,11 +818,11 @@ } }, "node_modules/@babel/generator": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", - "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dependencies": { - "@babel/types": "^7.23.0", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -1218,13 +856,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -1241,17 +879,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", - "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.7.tgz", + "integrity": "sha512-xCoqR/8+BoNnXOY7RVSgv6X+o7pmT5q1d+gGcRlXYkI+9B31glE4jeejhKVpA04O1AtzOt7OSQ6VYKP5FcRl9g==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-member-expression-to-functions": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", "semver": "^6.3.1" @@ -1299,9 +937,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz", - "integrity": "sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", + "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", "dev": true, "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -1369,9 +1007,9 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz", - "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-module-imports": "^7.22.15", @@ -1476,9 +1114,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "engines": { "node": ">=6.9.0" } @@ -1492,9 +1130,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", - "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", "engines": { "node": ">=6.9.0" } @@ -1514,22 +1152,22 @@ } }, "node_modules/@babel/helpers": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", - "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.8.tgz", + "integrity": "sha512-KDqYz4PiOWvDFrdHLPhKtCThtIcKVy6avWD2oG4GEvyQ+XDZwHD4YQd+H2vNMnq2rkdxsDkU82T+Vk8U/WXHRQ==", "dependencies": { "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0" + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", @@ -1540,9 +1178,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", "bin": { "parser": "bin/babel-parser.js" }, @@ -1551,9 +1189,9 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz", - "integrity": "sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz", + "integrity": "sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1566,14 +1204,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz", - "integrity": "sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz", + "integrity": "sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.15" + "@babel/plugin-transform-optional-chaining": "^7.23.3" }, "engines": { "node": ">=6.9.0" @@ -1582,6 +1220,22 @@ "@babel/core": "^7.13.0" } }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.7.tgz", + "integrity": "sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-proposal-private-property-in-object": { "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", @@ -1658,9 +1312,9 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", - "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz", + "integrity": "sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1673,9 +1327,9 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", - "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz", + "integrity": "sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1830,9 +1484,9 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", - "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz", + "integrity": "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1845,9 +1499,9 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.2.tgz", - "integrity": "sha512-BBYVGxbDVHfoeXbOwcagAkOQAm9NxoTdMGfTqghu1GrvadSaw6iW3Je6IcL5PNOw8VwjxqBECXy50/iCQSY/lQ==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.7.tgz", + "integrity": "sha512-PdxEpL71bJp1byMG0va5gwQcXHxuEYC/BgI/e88mGTtohbZN28O5Yit0Plkkm/dBzCF/BxmbNcses1RH1T+urA==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", @@ -1863,14 +1517,14 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", - "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", + "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", "dev": true, "dependencies": { - "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.5" + "@babel/helper-remap-async-to-generator": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -1880,9 +1534,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", - "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz", + "integrity": "sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1895,9 +1549,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.0.tgz", - "integrity": "sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz", + "integrity": "sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1910,12 +1564,12 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", - "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz", + "integrity": "sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1926,12 +1580,12 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz", - "integrity": "sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz", + "integrity": "sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-class-static-block": "^7.14.5" }, @@ -1943,18 +1597,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz", - "integrity": "sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==", + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.8.tgz", + "integrity": "sha512-yAYslGsY1bX6Knmg46RjiCiNSwJKv2IUC8qOdYKqMMr0491SXFhcHqOdRDeCRohOOIzwN/90C6mQ9qAKgrP7dg==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-split-export-declaration": "^7.22.6", "globals": "^11.1.0" }, @@ -1966,13 +1619,13 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", - "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz", + "integrity": "sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", - "@babel/template": "^7.22.5" + "@babel/template": "^7.22.15" }, "engines": { "node": ">=6.9.0" @@ -1982,9 +1635,9 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.0.tgz", - "integrity": "sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz", + "integrity": "sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1997,12 +1650,12 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", - "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz", + "integrity": "sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -2013,9 +1666,9 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", - "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz", + "integrity": "sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -2028,9 +1681,9 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz", - "integrity": "sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz", + "integrity": "sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -2044,12 +1697,12 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", - "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz", + "integrity": "sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==", "dev": true, "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -2060,9 +1713,9 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz", - "integrity": "sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz", + "integrity": "sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -2076,12 +1729,13 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz", - "integrity": "sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.6.tgz", + "integrity": "sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -2091,13 +1745,13 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", - "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz", + "integrity": "sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -2108,9 +1762,9 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz", - "integrity": "sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz", + "integrity": "sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -2124,9 +1778,9 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", - "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz", + "integrity": "sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -2139,9 +1793,9 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz", - "integrity": "sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz", + "integrity": "sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -2155,9 +1809,9 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", - "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz", + "integrity": "sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -2170,12 +1824,12 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.0.tgz", - "integrity": "sha512-xWT5gefv2HGSm4QHtgc1sYPbseOyf+FFDo2JbpE25GWl5BqTGO9IMwTYJRoIdjsF85GE+VegHxSCUt5EvoYTAw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz", + "integrity": "sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.23.0", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -2186,12 +1840,12 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.0.tgz", - "integrity": "sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz", + "integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.23.0", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-simple-access": "^7.22.5" }, @@ -2203,13 +1857,13 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.0.tgz", - "integrity": "sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.3.tgz", + "integrity": "sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==", "dev": true, "dependencies": { "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.23.0", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-validator-identifier": "^7.22.20" }, @@ -2221,12 +1875,12 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", - "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz", + "integrity": "sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -2253,9 +1907,9 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", - "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz", + "integrity": "sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -2268,9 +1922,9 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz", - "integrity": "sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz", + "integrity": "sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -2284,9 +1938,9 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz", - "integrity": "sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz", + "integrity": "sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -2300,16 +1954,16 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz", - "integrity": "sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz", + "integrity": "sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.22.9", + "@babel/compat-data": "^7.23.3", "@babel/helper-compilation-targets": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.22.15" + "@babel/plugin-transform-parameters": "^7.23.3" }, "engines": { "node": ">=6.9.0" @@ -2319,13 +1973,13 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", - "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz", + "integrity": "sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5" + "@babel/helper-replace-supers": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -2335,9 +1989,9 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz", - "integrity": "sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz", + "integrity": "sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -2351,9 +2005,9 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.0.tgz", - "integrity": "sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz", + "integrity": "sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -2368,9 +2022,9 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz", - "integrity": "sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz", + "integrity": "sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -2383,12 +2037,12 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", - "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz", + "integrity": "sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -2399,13 +2053,13 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz", - "integrity": "sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz", + "integrity": "sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, @@ -2417,9 +2071,9 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", - "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz", + "integrity": "sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -2432,9 +2086,9 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz", - "integrity": "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz", + "integrity": "sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -2448,9 +2102,9 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", - "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz", + "integrity": "sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -2463,16 +2117,16 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.2.tgz", - "integrity": "sha512-XOntj6icgzMS58jPVtQpiuF6ZFWxQiJavISGx5KGjRj+3gqZr8+N6Kx+N9BApWzgS+DOjIZfXXj0ZesenOWDyA==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.7.tgz", + "integrity": "sha512-fa0hnfmiXc9fq/weK34MUV0drz2pOL/vfKWvN7Qw127hiUPabFCUMgAbYWcchRzMJit4o5ARsK/s+5h0249pLw==", "dev": true, "dependencies": { "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", - "babel-plugin-polyfill-corejs2": "^0.4.6", - "babel-plugin-polyfill-corejs3": "^0.8.5", - "babel-plugin-polyfill-regenerator": "^0.5.3", + "babel-plugin-polyfill-corejs2": "^0.4.7", + "babel-plugin-polyfill-corejs3": "^0.8.7", + "babel-plugin-polyfill-regenerator": "^0.5.4", "semver": "^6.3.1" }, "engines": { @@ -2492,9 +2146,9 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", - "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz", + "integrity": "sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -2507,9 +2161,9 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", - "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz", + "integrity": "sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -2523,9 +2177,9 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", - "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz", + "integrity": "sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -2538,9 +2192,9 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", - "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz", + "integrity": "sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -2553,9 +2207,9 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", - "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz", + "integrity": "sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -2568,9 +2222,9 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz", - "integrity": "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz", + "integrity": "sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -2583,12 +2237,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", - "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz", + "integrity": "sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -2599,12 +2253,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", - "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz", + "integrity": "sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -2615,12 +2269,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", - "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz", + "integrity": "sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -2631,25 +2285,26 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.2.tgz", - "integrity": "sha512-BW3gsuDD+rvHL2VO2SjAUNTBe5YrjsTiDyqamPDWY723na3/yPQ65X5oQkFVJZ0o50/2d+svm1rkPoJeR1KxVQ==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.7.tgz", + "integrity": "sha512-SY27X/GtTz/L4UryMNJ6p4fH4nsgWbz84y9FE0bQeWJP6O5BhgVCt53CotQKHCOeXJel8VyhlhujhlltKms/CA==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.23.2", - "@babel/helper-compilation-targets": "^7.22.15", + "@babel/compat-data": "^7.23.5", + "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.15", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.15", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.22.5", - "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-assertions": "^7.23.3", + "@babel/plugin-syntax-import-attributes": "^7.23.3", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", @@ -2661,59 +2316,58 @@ "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.22.5", - "@babel/plugin-transform-async-generator-functions": "^7.23.2", - "@babel/plugin-transform-async-to-generator": "^7.22.5", - "@babel/plugin-transform-block-scoped-functions": "^7.22.5", - "@babel/plugin-transform-block-scoping": "^7.23.0", - "@babel/plugin-transform-class-properties": "^7.22.5", - "@babel/plugin-transform-class-static-block": "^7.22.11", - "@babel/plugin-transform-classes": "^7.22.15", - "@babel/plugin-transform-computed-properties": "^7.22.5", - "@babel/plugin-transform-destructuring": "^7.23.0", - "@babel/plugin-transform-dotall-regex": "^7.22.5", - "@babel/plugin-transform-duplicate-keys": "^7.22.5", - "@babel/plugin-transform-dynamic-import": "^7.22.11", - "@babel/plugin-transform-exponentiation-operator": "^7.22.5", - "@babel/plugin-transform-export-namespace-from": "^7.22.11", - "@babel/plugin-transform-for-of": "^7.22.15", - "@babel/plugin-transform-function-name": "^7.22.5", - "@babel/plugin-transform-json-strings": "^7.22.11", - "@babel/plugin-transform-literals": "^7.22.5", - "@babel/plugin-transform-logical-assignment-operators": "^7.22.11", - "@babel/plugin-transform-member-expression-literals": "^7.22.5", - "@babel/plugin-transform-modules-amd": "^7.23.0", - "@babel/plugin-transform-modules-commonjs": "^7.23.0", - "@babel/plugin-transform-modules-systemjs": "^7.23.0", - "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-arrow-functions": "^7.23.3", + "@babel/plugin-transform-async-generator-functions": "^7.23.7", + "@babel/plugin-transform-async-to-generator": "^7.23.3", + "@babel/plugin-transform-block-scoped-functions": "^7.23.3", + "@babel/plugin-transform-block-scoping": "^7.23.4", + "@babel/plugin-transform-class-properties": "^7.23.3", + "@babel/plugin-transform-class-static-block": "^7.23.4", + "@babel/plugin-transform-classes": "^7.23.5", + "@babel/plugin-transform-computed-properties": "^7.23.3", + "@babel/plugin-transform-destructuring": "^7.23.3", + "@babel/plugin-transform-dotall-regex": "^7.23.3", + "@babel/plugin-transform-duplicate-keys": "^7.23.3", + "@babel/plugin-transform-dynamic-import": "^7.23.4", + "@babel/plugin-transform-exponentiation-operator": "^7.23.3", + "@babel/plugin-transform-export-namespace-from": "^7.23.4", + "@babel/plugin-transform-for-of": "^7.23.6", + "@babel/plugin-transform-function-name": "^7.23.3", + "@babel/plugin-transform-json-strings": "^7.23.4", + "@babel/plugin-transform-literals": "^7.23.3", + "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", + "@babel/plugin-transform-member-expression-literals": "^7.23.3", + "@babel/plugin-transform-modules-amd": "^7.23.3", + "@babel/plugin-transform-modules-commonjs": "^7.23.3", + "@babel/plugin-transform-modules-systemjs": "^7.23.3", + "@babel/plugin-transform-modules-umd": "^7.23.3", "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.22.5", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", - "@babel/plugin-transform-numeric-separator": "^7.22.11", - "@babel/plugin-transform-object-rest-spread": "^7.22.15", - "@babel/plugin-transform-object-super": "^7.22.5", - "@babel/plugin-transform-optional-catch-binding": "^7.22.11", - "@babel/plugin-transform-optional-chaining": "^7.23.0", - "@babel/plugin-transform-parameters": "^7.22.15", - "@babel/plugin-transform-private-methods": "^7.22.5", - "@babel/plugin-transform-private-property-in-object": "^7.22.11", - "@babel/plugin-transform-property-literals": "^7.22.5", - "@babel/plugin-transform-regenerator": "^7.22.10", - "@babel/plugin-transform-reserved-words": "^7.22.5", - "@babel/plugin-transform-shorthand-properties": "^7.22.5", - "@babel/plugin-transform-spread": "^7.22.5", - "@babel/plugin-transform-sticky-regex": "^7.22.5", - "@babel/plugin-transform-template-literals": "^7.22.5", - "@babel/plugin-transform-typeof-symbol": "^7.22.5", - "@babel/plugin-transform-unicode-escapes": "^7.22.10", - "@babel/plugin-transform-unicode-property-regex": "^7.22.5", - "@babel/plugin-transform-unicode-regex": "^7.22.5", - "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.23.3", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", + "@babel/plugin-transform-numeric-separator": "^7.23.4", + "@babel/plugin-transform-object-rest-spread": "^7.23.4", + "@babel/plugin-transform-object-super": "^7.23.3", + "@babel/plugin-transform-optional-catch-binding": "^7.23.4", + "@babel/plugin-transform-optional-chaining": "^7.23.4", + "@babel/plugin-transform-parameters": "^7.23.3", + "@babel/plugin-transform-private-methods": "^7.23.3", + "@babel/plugin-transform-private-property-in-object": "^7.23.4", + "@babel/plugin-transform-property-literals": "^7.23.3", + "@babel/plugin-transform-regenerator": "^7.23.3", + "@babel/plugin-transform-reserved-words": "^7.23.3", + "@babel/plugin-transform-shorthand-properties": "^7.23.3", + "@babel/plugin-transform-spread": "^7.23.3", + "@babel/plugin-transform-sticky-regex": "^7.23.3", + "@babel/plugin-transform-template-literals": "^7.23.3", + "@babel/plugin-transform-typeof-symbol": "^7.23.3", + "@babel/plugin-transform-unicode-escapes": "^7.23.3", + "@babel/plugin-transform-unicode-property-regex": "^7.23.3", + "@babel/plugin-transform-unicode-regex": "^7.23.3", + "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", "@babel/preset-modules": "0.1.6-no-external-plugins", - "@babel/types": "^7.23.0", - "babel-plugin-polyfill-corejs2": "^0.4.6", - "babel-plugin-polyfill-corejs3": "^0.8.5", - "babel-plugin-polyfill-regenerator": "^0.5.3", + "babel-plugin-polyfill-corejs2": "^0.4.7", + "babel-plugin-polyfill-corejs3": "^0.8.7", + "babel-plugin-polyfill-regenerator": "^0.5.4", "core-js-compat": "^3.31.0", "semver": "^6.3.1" }, @@ -2754,9 +2408,9 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", - "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.7.tgz", + "integrity": "sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==", "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -2779,19 +2433,19 @@ } }, "node_modules/@babel/traverse": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", - "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz", + "integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==", "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", - "debug": "^4.1.0", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -2799,11 +2453,11 @@ } }, "node_modules/@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dependencies": { - "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, @@ -2842,10 +2496,26 @@ "node": ">=10.0.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz", + "integrity": "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz", + "integrity": "sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==", "cpu": [ "arm" ], @@ -2859,9 +2529,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz", + "integrity": "sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==", "cpu": [ "arm64" ], @@ -2875,9 +2545,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz", + "integrity": "sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==", "cpu": [ "x64" ], @@ -2891,9 +2561,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz", + "integrity": "sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==", "cpu": [ "arm64" ], @@ -2907,9 +2577,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz", + "integrity": "sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==", "cpu": [ "x64" ], @@ -2923,9 +2593,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz", + "integrity": "sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==", "cpu": [ "arm64" ], @@ -2939,9 +2609,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz", + "integrity": "sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==", "cpu": [ "x64" ], @@ -2955,9 +2625,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz", + "integrity": "sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==", "cpu": [ "arm" ], @@ -2971,9 +2641,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz", + "integrity": "sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==", "cpu": [ "arm64" ], @@ -2987,9 +2657,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz", + "integrity": "sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==", "cpu": [ "ia32" ], @@ -3003,9 +2673,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz", + "integrity": "sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==", "cpu": [ "loong64" ], @@ -3019,9 +2689,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz", + "integrity": "sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==", "cpu": [ "mips64el" ], @@ -3035,9 +2705,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz", + "integrity": "sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==", "cpu": [ "ppc64" ], @@ -3051,9 +2721,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz", + "integrity": "sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==", "cpu": [ "riscv64" ], @@ -3067,9 +2737,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz", + "integrity": "sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==", "cpu": [ "s390x" ], @@ -3083,9 +2753,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz", + "integrity": "sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==", "cpu": [ "x64" ], @@ -3099,9 +2769,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz", + "integrity": "sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==", "cpu": [ "x64" ], @@ -3115,9 +2785,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz", + "integrity": "sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==", "cpu": [ "x64" ], @@ -3131,9 +2801,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz", + "integrity": "sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==", "cpu": [ "x64" ], @@ -3147,9 +2817,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz", + "integrity": "sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==", "cpu": [ "arm64" ], @@ -3163,9 +2833,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz", + "integrity": "sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==", "cpu": [ "ia32" ], @@ -3179,9 +2849,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz", + "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==", "cpu": [ "x64" ], @@ -3318,18 +2988,18 @@ } }, "node_modules/@fastify/busboy": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz", - "integrity": "sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", + "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", "dev": true, "engines": { "node": ">=14" } }, "node_modules/@fortawesome/fontawesome-free": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.2.tgz", - "integrity": "sha512-m5cPn3e2+FDCOgi1mz0RexTUvvQibBebOUlUlW0+YrMjDTPkiJ6VTKukA1GRsvRw+12KyJndNjj0O4AgTxm2Pg==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.1.tgz", + "integrity": "sha512-CNy5vSwN3fsUStPRLX7fUYojyuzoEMSXPl7zSLJ8TgtRfjv24LOnOWKT2zYwaHZCJGkdyRnTmstR0P+Ah503Gw==", "hasInstallScript": true, "engines": { "node": ">=6" @@ -3715,9 +3385,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "17.0.7", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.0.7.tgz", - "integrity": "sha512-gwhUhpwXn0trwwKdSu9WlJbEcLt+s/2fPwoD9lZ0y3wXfrOogsfcNBJKeO5BZf1h+A3AWt7ePmgrZXSJM+865Q==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.1.0.tgz", + "integrity": "sha512-FAp5Vh4Y4DFDnrxEitggEkeDwHCml7m6hZUgohvA6n6mwrMT0ZZXnk3MIrKRnT6A9cr1wcnxMW+jIXx/cJZGlw==", "dev": true, "engines": { "node": "^18.13.0 || >=20.9.0", @@ -3726,7 +3396,7 @@ }, "peerDependencies": { "@angular/compiler-cli": "^17.0.0", - "typescript": ">=5.2 <5.3", + "typescript": ">=5.2 <5.4", "webpack": "^5.54.0" } }, @@ -3779,9 +3449,9 @@ } }, "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", - "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", "dev": true, "engines": { "node": "14 || >=16.14" @@ -3800,9 +3470,9 @@ } }, "node_modules/@npmcli/git": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.3.tgz", - "integrity": "sha512-UZp9NwK+AynTrKvHn5k3KviW/hA5eENmFsu3iAPe7sWRt0lFUdsY/wXIYjpDFe7cdSNwOIzbObfwgt6eL5/2zw==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.4.tgz", + "integrity": "sha512-nr6/WezNzuYUppzXRaYu/W4aT5rLxdXqEFupbh6e/ovlYFQ8hpu1UUPV3Ir/YTl+74iXl2ZOMlGzudh9ZPUchQ==", "dev": true, "dependencies": { "@npmcli/promise-spawn": "^7.0.0", @@ -3828,9 +3498,9 @@ } }, "node_modules/@npmcli/git/node_modules/lru-cache": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", - "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", "dev": true, "engines": { "node": "14 || >=16.14" @@ -3877,9 +3547,9 @@ } }, "node_modules/@npmcli/promise-spawn": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.0.tgz", - "integrity": "sha512-wBqcGsMELZna0jDblGd7UXgOby45TQaMWmbFwWX+SEotk4HV6zG2t6rT9siyLhPk4P6YYqgfL1UO8nMWDBVJXQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.1.tgz", + "integrity": "sha512-P4KkF9jX3y+7yFUxgcUdDtLy+t4OlDGuEBLNs57AZsfSfg+uV6MLndqGpnl4831ggaEdXwR50XFoZP4VFtHolg==", "dev": true, "dependencies": { "which": "^4.0.0" @@ -3913,9 +3583,9 @@ } }, "node_modules/@npmcli/run-script": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-7.0.2.tgz", - "integrity": "sha512-Omu0rpA8WXvcGeY6DDzyRoY1i5DkCBkzyJ+m2u7PD6quzb0TvSqdIPOkTn8ZBOj7LbbcbMfZ3c5skwSu6m8y2w==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-7.0.3.tgz", + "integrity": "sha512-ZMWGLHpzMq3rBGIwPyeaoaleaLMvrBrH8nugHxTi5ACkJZXTxXPtVuEH91ifgtss5hUwJQ2VDnzDBWPmz78rvg==", "dev": true, "dependencies": { "@npmcli/node-gyp": "^3.0.0", @@ -3953,21 +3623,21 @@ } }, "node_modules/@nrwl/devkit": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-17.1.3.tgz", - "integrity": "sha512-8HfIY7P3yIYfQ/XKuHoq0GGLA9GpwWtBlI9kPQ0ygjuJ9BkpiGMtQvO6003zs7c6vpc2vNeG+Jmi72+EKvoN5A==", + "version": "17.2.8", + "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-17.2.8.tgz", + "integrity": "sha512-l2dFy5LkWqSA45s6pee6CoqJeluH+sjRdVnAAQfjLHRNSx6mFAKblyzq5h1f4P0EUCVVVqLs+kVqmNx5zxYqvw==", "dev": true, "dependencies": { - "@nx/devkit": "17.1.3" + "@nx/devkit": "17.2.8" } }, "node_modules/@nrwl/tao": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/@nrwl/tao/-/tao-17.1.3.tgz", - "integrity": "sha512-9YpfEkUpVqOweqgQvMDcWApNx4jhCqBNH5IByZj302Enp3TLnQSvhuX5Dfr8hNQRQokIpEn6tW8SGTctTM5LXw==", + "version": "17.2.8", + "resolved": "https://registry.npmjs.org/@nrwl/tao/-/tao-17.2.8.tgz", + "integrity": "sha512-Qpk5YKeJ+LppPL/wtoDyNGbJs2MsTi6qyX/RdRrEc8lc4bk6Cw3Oul1qTXCI6jT0KzTz+dZtd0zYD/G7okkzvg==", "dev": true, "dependencies": { - "nx": "17.1.3", + "nx": "17.2.8", "tslib": "^2.3.0" }, "bin": { @@ -3975,12 +3645,12 @@ } }, "node_modules/@nx/devkit": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-17.1.3.tgz", - "integrity": "sha512-1Is7ooovg3kdGJ5VdkePulRUDaMYLLULr+LwXgx7oHSW7AY2iCmhkoOE/vSR7DJ6rkey2gYx7eT1IoRoORiIaQ==", + "version": "17.2.8", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-17.2.8.tgz", + "integrity": "sha512-6LtiQihtZwqz4hSrtT5cCG5XMCWppG6/B8c1kNksg97JuomELlWyUyVF+sxmeERkcLYFaKPTZytP0L3dmCFXaw==", "dev": true, "dependencies": { - "@nrwl/devkit": "17.1.3", + "@nrwl/devkit": "17.2.8", "ejs": "^3.1.7", "enquirer": "~2.3.6", "ignore": "^5.0.4", @@ -3992,10 +3662,43 @@ "nx": ">= 16 <= 18" } }, + "node_modules/@nx/devkit/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@nx/devkit/node_modules/semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@nx/devkit/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/@nx/nx-darwin-arm64": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-17.1.3.tgz", - "integrity": "sha512-f4qLa0y3C4uuhYKgq+MU892WaQvtvmHqrEhHINUOxYXNiLy2sgyJPW0mOZvzXtC4dPaUmiVaFP5RMVzc8Lxhtg==", + "version": "17.2.8", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-17.2.8.tgz", + "integrity": "sha512-dMb0uxug4hM7tusISAU1TfkDK3ixYmzc1zhHSZwpR7yKJIyKLtUpBTbryt8nyso37AS1yH+dmfh2Fj2WxfBHTg==", "cpu": [ "arm64" ], @@ -4009,9 +3712,9 @@ } }, "node_modules/@nx/nx-darwin-x64": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-17.1.3.tgz", - "integrity": "sha512-kh76ZjqkLeQUIAfTa9G/DFFf+e1sZ5ipDzk7zFGhZ2k68PoQoFdsFOO3C513JmuEdavspts6Hkifsqh61TaE+A==", + "version": "17.2.8", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-17.2.8.tgz", + "integrity": "sha512-0cXzp1tGr7/6lJel102QiLA4NkaLCkQJj6VzwbwuvmuCDxPbpmbz7HC1tUteijKBtOcdXit1/MEoEU007To8Bw==", "cpu": [ "x64" ], @@ -4025,9 +3728,9 @@ } }, "node_modules/@nx/nx-freebsd-x64": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-17.1.3.tgz", - "integrity": "sha512-CRuVL5ZSLb+Gc8vwMUUe9Pl/1Z26YtXMKTahBMQh2dac63vzLgzqIV4c66aduUl1x2M0kGYBSIIRG9z0/BgWeg==", + "version": "17.2.8", + "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-17.2.8.tgz", + "integrity": "sha512-YFMgx5Qpp2btCgvaniDGdu7Ctj56bfFvbbaHQWmOeBPK1krNDp2mqp8HK6ZKOfEuDJGOYAp7HDtCLvdZKvJxzA==", "cpu": [ "x64" ], @@ -4041,9 +3744,9 @@ } }, "node_modules/@nx/nx-linux-arm-gnueabihf": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-17.1.3.tgz", - "integrity": "sha512-KDBmd5tSrg93g/oij/eGW4yeVNVK3DBIM4VYAS2vtkIgVOGoqcQ+SEIeMK3nMUJP9jGyblt3QNj5ZsJBtScwQw==", + "version": "17.2.8", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-17.2.8.tgz", + "integrity": "sha512-iN2my6MrhLRkVDtdivQHugK8YmR7URo1wU9UDuHQ55z3tEcny7LV3W9NSsY9UYPK/FrxdDfevj0r2hgSSdhnzA==", "cpu": [ "arm" ], @@ -4057,9 +3760,9 @@ } }, "node_modules/@nx/nx-linux-arm64-gnu": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-17.1.3.tgz", - "integrity": "sha512-W2tNL/7sIwoQKLmuy68Usd6TZzIZvxZt4UE30kDwGc2RSap6RCHAvDbzSxtW+L4+deC9UxX0Tty0VuW+J8FjSg==", + "version": "17.2.8", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-17.2.8.tgz", + "integrity": "sha512-Iy8BjoW6mOKrSMiTGujUcNdv+xSM1DALTH6y3iLvNDkGbjGK1Re6QNnJAzqcXyDpv32Q4Fc57PmuexyysZxIGg==", "cpu": [ "arm64" ], @@ -4073,9 +3776,9 @@ } }, "node_modules/@nx/nx-linux-arm64-musl": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-17.1.3.tgz", - "integrity": "sha512-Oto3gkLd7yweuVUCsSHwm4JkAIbcxpPJP0ycRHI/PRHPMIOPiMX8r651QM1amMyKAbJtAe047nyb9Sh1X0FA4A==", + "version": "17.2.8", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-17.2.8.tgz", + "integrity": "sha512-9wkAxWzknjpzdofL1xjtU6qPFF1PHlvKCZI3hgEYJDo4mQiatGI+7Ttko+lx/ZMP6v4+Umjtgq7+qWrApeKamQ==", "cpu": [ "arm64" ], @@ -4089,9 +3792,9 @@ } }, "node_modules/@nx/nx-linux-x64-gnu": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-17.1.3.tgz", - "integrity": "sha512-pJS994sa5PBPFak93RydTB9KdEmiVb3rgiSB7PDBegphERbzHEB77B7G8M5TZ62dGlMdplIEKmdhY5XNqeAf9A==", + "version": "17.2.8", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-17.2.8.tgz", + "integrity": "sha512-sjG1bwGsjLxToasZ3lShildFsF0eyeGu+pOQZIp9+gjFbeIkd19cTlCnHrOV9hoF364GuKSXQyUlwtFYFR4VTQ==", "cpu": [ "x64" ], @@ -4105,9 +3808,9 @@ } }, "node_modules/@nx/nx-linux-x64-musl": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-17.1.3.tgz", - "integrity": "sha512-4Hcx5Fg/88jV+bcTr6P0dM4unXNvKgrGJe3oK9/sgEhiW6pD2UAFjv16CCSRcWhDUAzUDqcwnD2fgg+vnAJG6g==", + "version": "17.2.8", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-17.2.8.tgz", + "integrity": "sha512-QiakXZ1xBCIptmkGEouLHQbcM4klQkcr+kEaz2PlNwy/sW3gH1b/1c0Ed5J1AN9xgQxWspriAONpScYBRgxdhA==", "cpu": [ "x64" ], @@ -4121,9 +3824,9 @@ } }, "node_modules/@nx/nx-win32-arm64-msvc": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-17.1.3.tgz", - "integrity": "sha512-dUasEuskmDxUL36XA0GZqSb9233suE4wKhxrMobyFBzHUZ2tq/unzOpPjYfqDBie4QIvF8tEpAjQsLds8LWgbw==", + "version": "17.2.8", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-17.2.8.tgz", + "integrity": "sha512-XBWUY/F/GU3vKN9CAxeI15gM4kr3GOBqnzFZzoZC4qJt2hKSSUEWsMgeZtsMgeqEClbi4ZyCCkY7YJgU32WUGA==", "cpu": [ "arm64" ], @@ -4137,9 +3840,9 @@ } }, "node_modules/@nx/nx-win32-x64-msvc": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-17.1.3.tgz", - "integrity": "sha512-eTuTpBHFvA5NFJh/iosmqCL4JOAjDrwXLSMgfKrZKjiApHMG1T/5Hb+PrsNpt+WnGp94ur7c4Dtx4xD5vlpAEw==", + "version": "17.2.8", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-17.2.8.tgz", + "integrity": "sha512-HTqDv+JThlLzbcEm/3f+LbS5/wYQWzb5YDXbP1wi7nlCTihNZOLNqGOkEmwlrR5tAdNHPRpHSmkYg4305W0CtA==", "cpu": [ "x64" ], @@ -4176,14 +3879,183 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz", + "integrity": "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz", + "integrity": "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz", + "integrity": "sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz", + "integrity": "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz", + "integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz", + "integrity": "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz", + "integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz", + "integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz", + "integrity": "sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.6.tgz", + "integrity": "sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz", + "integrity": "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz", + "integrity": "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz", + "integrity": "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@schematics/angular": { - "version": "17.0.7", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.0.7.tgz", - "integrity": "sha512-d7QKmcKrM4owb/2bR7Ipf23roiNbvbD/x7reNhQAtKAPLSHJ3Ulkf1+Yv+dj+9f+K7y9SBviEUSrD27BQ9WaxQ==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.1.0.tgz", + "integrity": "sha512-u9pCesRWb6mVtLnFLSfZ8R21TDz8YCebAxViefWsJlb0+p0yknesVL1nG/Oi9tgfhczS991HGIVsLT41bZthUw==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.0.7", - "@angular-devkit/schematics": "17.0.7", + "@angular-devkit/core": "17.1.0", + "@angular-devkit/schematics": "17.1.0", "jsonc-parser": "3.2.0" }, "engines": { @@ -4193,9 +4065,9 @@ } }, "node_modules/@sigstore/bundle": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.1.0.tgz", - "integrity": "sha512-89uOo6yh/oxaU8AeOUnVrTdVMcGk9Q1hJa7Hkvalc6G3Z3CupWk4Xe9djSgJm9fMkH69s0P0cVHUoKSOemLdng==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.1.1.tgz", + "integrity": "sha512-v3/iS+1nufZdKQ5iAlQKcCsoh0jffQyABvYIxKsZQFWc4ubuGjwZklFHpDgV6O6T7vvV78SW5NHI91HFKEcxKg==", "dev": true, "dependencies": { "@sigstore/protobuf-specs": "^0.2.1" @@ -4204,6 +4076,15 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/@sigstore/core": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-0.2.0.tgz", + "integrity": "sha512-THobAPPZR9pDH2CAvDLpkrYedt7BlZnsyxDe+Isq4ZmGfPy5juOFZq487vCU2EgKD7aHSiTfE/i7sN7aEdzQnA==", + "dev": true, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, "node_modules/@sigstore/protobuf-specs": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.2.1.tgz", @@ -4214,12 +4095,13 @@ } }, "node_modules/@sigstore/sign": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.2.0.tgz", - "integrity": "sha512-AAbmnEHDQv6CSfrWA5wXslGtzLPtAtHZleKOgxdQYvx/s76Fk6T6ZVt7w2IGV9j1UrFeBocTTQxaXG2oRrDhYA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.2.1.tgz", + "integrity": "sha512-U5sKQEj+faE1MsnLou1f4DQQHeFZay+V9s9768lw48J4pKykPj34rWyI1lsMOGJ3Mae47Ye6q3HAJvgXO21rkQ==", "dev": true, "dependencies": { - "@sigstore/bundle": "^2.1.0", + "@sigstore/bundle": "^2.1.1", + "@sigstore/core": "^0.2.0", "@sigstore/protobuf-specs": "^0.2.1", "make-fetch-happen": "^13.0.0" }, @@ -4228,13 +4110,27 @@ } }, "node_modules/@sigstore/tuf": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.2.0.tgz", - "integrity": "sha512-KKATZ5orWfqd9ZG6MN8PtCIx4eevWSuGRKQvofnWXRpyMyUEpmrzg5M5BrCpjM+NfZ0RbNGOh5tCz/P2uoRqOA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.0.tgz", + "integrity": "sha512-S98jo9cpJwO1mtQ+2zY7bOdcYyfVYCUaofCG6wWRzk3pxKHVAkSfshkfecto2+LKsx7Ovtqbgb2LS8zTRhxJ9Q==", "dev": true, "dependencies": { "@sigstore/protobuf-specs": "^0.2.1", - "tuf-js": "^2.1.0" + "tuf-js": "^2.2.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-0.1.0.tgz", + "integrity": "sha512-2UzMNYAa/uaz11NhvgRnIQf4gpLTJ59bhb8ESXaoSS5sxedfS+eLak8bsdMc+qpNQfITUTFoSKFx5h8umlRRiA==", + "dev": true, + "dependencies": { + "@sigstore/bundle": "^2.1.1", + "@sigstore/core": "^0.2.0", + "@sigstore/protobuf-specs": "^0.2.1" }, "engines": { "node": "^16.14.0 || >=18.0.0" @@ -4246,12 +4142,6 @@ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", - "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==", - "dev": true - }, "node_modules/@swimlane/ngx-charts": { "version": "20.5.0", "resolved": "https://registry.npmjs.org/@swimlane/ngx-charts/-/ngx-charts-20.5.0.tgz", @@ -4359,6 +4249,43 @@ "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-21.0.0.tgz", "integrity": "sha512-qVfOiFh0U8ZSkLgA6tf7kj2MciqRbSCWaJZRwftVO7UbtVDNsZAXpWXqvCDtIefvjC83UJB+vHTDOGm5ibXjEA==" }, + "node_modules/@types/babel__core": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.2.tgz", + "integrity": "sha512-pNpr1T1xLUc2l3xJKuPtsEky3ybxN3m4fJkknfIpTCTfIZCDW57oAg+EfCgIIp2rvCe0Wn++/FfodDS4YXxBwA==", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -4397,21 +4324,6 @@ "@types/node": "*" } }, - "node_modules/@types/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", - "dev": true - }, - "node_modules/@types/cors": { - "version": "2.8.16", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.16.tgz", - "integrity": "sha512-Trx5or1Nyg1Fq138PCuWqoApzvoSLWzZ25ORBiHMbbUT42g578lH1GT4TwYDbiUOLFuDsCkfLneT2105fsFWGg==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/d3": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", @@ -4686,9 +4598,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", - "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, "node_modules/@types/express": { @@ -4749,9 +4661,9 @@ "dev": true }, "node_modules/@types/luxon": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.7.tgz", - "integrity": "sha512-gKc9P2d4g5uYwmy4s/MO/yOVPmvHyvzka1YH6i5dM03UrFofHSmgc0D0ymbDRStFWHusk6cwwF6nhLm/ckBbbQ==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.0.tgz", + "integrity": "sha512-PEVoA4MOfSsFNaPrZjIUGUZujBDxnO/tj2A2N9KfzlR+pNgpBdDuk0TmRvSMAVUP5q4q8IkMEZ8UOp3MIr+QgA==", "dev": true }, "node_modules/@types/mime": { @@ -4770,18 +4682,18 @@ } }, "node_modules/@types/node-forge": { - "version": "1.3.10", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.10.tgz", - "integrity": "sha512-y6PJDYN4xYBxwd22l+OVH35N+1fCYWiuC3aiP2SlXVE6Lo7SS+rSx9r89hLxrP4pn6n1lBGhHJ12pj3F3Mpttw==", + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/qs": { - "version": "6.9.10", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz", - "integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==", + "version": "6.9.11", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", + "integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==", "dev": true }, "node_modules/@types/range-parser": { @@ -5011,49 +4923,16 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@typescript-eslint/parser": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.0.tgz", - "integrity": "sha512-VpG+M7GNhHLI/aTDctqAV0XbzB16vf+qDX9DXuMZSe/0bahzDA9AKZB15NDbd+D9M4cDsJvfkbGOA7qiZ/bWJw==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.19.0.tgz", + "integrity": "sha512-1DyBLG5SH7PYCd00QlroiW60YJ4rWMuUGa/JBV0iZuqi4l4IK3twKPq5ZkEebmGqRjXWVgsUzfd3+nZveewgow==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.13.0", - "@typescript-eslint/types": "6.13.0", - "@typescript-eslint/typescript-estree": "6.13.0", - "@typescript-eslint/visitor-keys": "6.13.0", + "@typescript-eslint/scope-manager": "6.19.0", + "@typescript-eslint/types": "6.19.0", + "@typescript-eslint/typescript-estree": "6.19.0", + "@typescript-eslint/visitor-keys": "6.19.0", "debug": "^4.3.4" }, "engines": { @@ -5072,121 +4951,14 @@ } } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.0.tgz", - "integrity": "sha512-2x0K2/CujsokIv+LN2T0l5FVDMtsCjkUyYtlcY4xxnxLAW+x41LXr16duoicHpGtLhmtN7kqvuFJ3zbz00Ikhw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.13.0", - "@typescript-eslint/visitor-keys": "6.13.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.0.tgz", - "integrity": "sha512-oXg7DFxx/GmTrKXKKLSoR2rwiutOC7jCQ5nDH5p5VS6cmHE1TcPTaYQ0VPSSUvj7BnNqCgQ/NXcTBxn59pfPTQ==", - "dev": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.0.tgz", - "integrity": "sha512-IT4O/YKJDoiy/mPEDsfOfp+473A9GVqXlBKckfrAOuVbTqM8xbc0LuqyFCcgeFWpqu3WjQexolgqN2CuWBYbog==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.13.0", - "@typescript-eslint/visitor-keys": "6.13.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.0.tgz", - "integrity": "sha512-UQklteCEMCRoq/1UhKFZsHv5E4dN1wQSzJoxTfABasWk1HgJRdg1xNUve/Kv/Sdymt4x+iEzpESOqRFlQr/9Aw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.13.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz", - "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.19.0.tgz", + "integrity": "sha512-dO1XMhV2ehBI6QN8Ufi7I10wmUovmLU0Oru3n5LVlM2JuzB4M+dVphCPLkVpKvGij2j/pHBWuJ9piuXx+BhzxQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.13.1", - "@typescript-eslint/visitor-keys": "6.13.1" + "@typescript-eslint/types": "6.19.0", + "@typescript-eslint/visitor-keys": "6.19.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -5197,13 +4969,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz", - "integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.19.0.tgz", + "integrity": "sha512-mcvS6WSWbjiSxKCwBcXtOM5pRkPQ6kcDds/juxcy/727IQr3xMEcwr/YLHW2A2+Fp5ql6khjbKBzOyjuPqGi/w==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.13.1", - "@typescript-eslint/utils": "6.13.1", + "@typescript-eslint/typescript-estree": "6.19.0", + "@typescript-eslint/utils": "6.19.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -5224,9 +4996,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz", - "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.0.tgz", + "integrity": "sha512-lFviGV/vYhOy3m8BJ/nAKoAyNhInTdXpftonhWle66XHAtT1ouBlkjL496b5H5hb8dWXHwtypTqgtb/DEa+j5A==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -5237,16 +5009,17 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz", - "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.0.tgz", + "integrity": "sha512-o/zefXIbbLBZ8YJ51NlkSAt2BamrK6XOmuxSR3hynMIzzyMY33KuJ9vuMdFSXW+H0tVvdF9qBPTHA91HDb4BIQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.13.1", - "@typescript-eslint/visitor-keys": "6.13.1", + "@typescript-eslint/types": "6.19.0", + "@typescript-eslint/visitor-keys": "6.19.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", + "minimatch": "9.0.3", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" }, @@ -5263,51 +5036,42 @@ } } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" + "balanced-match": "^1.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dev": true, "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@typescript-eslint/utils": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz", - "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.19.0.tgz", + "integrity": "sha512-QR41YXySiuN++/dC9UArYOg4X86OAYP83OWTewpVx5ct1IZhjjgTLocj7QNxGhWoTqknsgpl7L+hGygCO+sdYw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.13.1", - "@typescript-eslint/types": "6.13.1", - "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/scope-manager": "6.19.0", + "@typescript-eslint/types": "6.19.0", + "@typescript-eslint/typescript-estree": "6.19.0", "semver": "^7.5.4" }, "engines": { @@ -5321,46 +5085,13 @@ "eslint": "^7.0.0 || ^8.0.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", - "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.0.tgz", + "integrity": "sha512-hZaUCORLgubBvtGpp1JEFEazcuEdfxta9j4iUwdSAr7mEsYYAp3EAUyCZk3VEEqGj6W+AV4uWyrDGtrlawAsgQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/types": "6.19.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -5378,15 +5109,15 @@ "dev": true }, "node_modules/@vitejs/plugin-basic-ssl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.0.1.tgz", - "integrity": "sha512-pcub+YbFtFhaGRTo1832FQHQSHvMrlb43974e2eS8EKleR3p1cDdkJFPci1UhwkEf1J9Bz+wKBSzqpKp7nNj2A==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.0.2.tgz", + "integrity": "sha512-DKHKVtpI+eA5fvObVgQ3QtTGU70CcCnedalzqmGSR050AzKZMdUzgC8KmlOneHWH8dF2hJ3wkC9+8FDVAaDRCw==", "dev": true, "engines": { "node": ">=14.6.0" }, "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" } }, "node_modules/@webassemblyjs/ast": { @@ -5584,12 +5315,6 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "node_modules/abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "dev": true - }, "node_modules/abbrev": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", @@ -5852,9 +5577,9 @@ } }, "node_modules/array-flatten": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "dev": true }, "node_modules/array-union": { @@ -5872,15 +5597,6 @@ "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", "dev": true }, - "node_modules/async-each-series": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/async-each-series/-/async-each-series-0.1.1.tgz", - "integrity": "sha512-p4jj6Fws4Iy2m0iCmI2am2ZNZCgbdgE+P8F/8csmn2vx7ixXrO2zGcuNsD46X5uZSVecmkEy/M06X2vG8KD6dQ==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -5924,15 +5640,6 @@ "postcss": "^8.1.0" } }, - "node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "dev": true, - "dependencies": { - "follow-redirects": "^1.14.0" - } - }, "node_modules/axobject-query": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz", @@ -5976,13 +5683,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz", - "integrity": "sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==", + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz", + "integrity": "sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg==", "dev": true, "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.4.3", + "@babel/helper-define-polyfill-provider": "^0.5.0", "semver": "^6.3.1" }, "peerDependencies": { @@ -5999,25 +5706,41 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.8.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.6.tgz", - "integrity": "sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ==", + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.7.tgz", + "integrity": "sha512-KyDvZYxAzkC0Aj2dAPyDzi2Ym15e5JKZSK+maI7NAwSqofvuFglbSsxE7wUOvTg9oFVnHMzVzBKcqEb4PJgtOA==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.3", + "@babel/helper-define-polyfill-provider": "^0.4.4", "core-js-compat": "^3.33.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.3.tgz", - "integrity": "sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==", + "node_modules/babel-plugin-polyfill-corejs3/node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.4.tgz", + "integrity": "sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.3" + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz", + "integrity": "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.5.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -6047,15 +5770,6 @@ } ] }, - "node_modules/base64id": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", - "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", - "dev": true, - "engines": { - "node": "^4.5.0 || >= 5.9" - } - }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -6114,6 +5828,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/body-parser/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/body-parser/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -6123,57 +5846,18 @@ "ms": "2.0.0" } }, - "node_modules/body-parser/node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, - "node_modules/body-parser/node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/body-parser/node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dev": true, - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/bonjour-service": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", - "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", + "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", "dev": true, "dependencies": { - "array-flatten": "^2.1.2", - "dns-equal": "^1.0.0", "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } @@ -6223,269 +5907,10 @@ "node": ">=8" } }, - "node_modules/browser-sync": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.29.3.tgz", - "integrity": "sha512-NiM38O6XU84+MN+gzspVmXV2fTOoe+jBqIBx3IBdhZrdeURr6ZgznJr/p+hQ+KzkKEiGH/GcC4SQFSL0jV49bg==", - "dev": true, - "dependencies": { - "browser-sync-client": "^2.29.3", - "browser-sync-ui": "^2.29.3", - "bs-recipes": "1.3.4", - "chalk": "4.1.2", - "chokidar": "^3.5.1", - "connect": "3.6.6", - "connect-history-api-fallback": "^1", - "dev-ip": "^1.0.1", - "easy-extender": "^2.3.4", - "eazy-logger": "^4.0.1", - "etag": "^1.8.1", - "fresh": "^0.5.2", - "fs-extra": "3.0.1", - "http-proxy": "^1.18.1", - "immutable": "^3", - "localtunnel": "^2.0.1", - "micromatch": "^4.0.2", - "opn": "5.3.0", - "portscanner": "2.2.0", - "raw-body": "^2.3.2", - "resp-modifier": "6.0.2", - "rx": "4.1.0", - "send": "0.16.2", - "serve-index": "1.9.1", - "serve-static": "1.13.2", - "server-destroy": "1.0.1", - "socket.io": "^4.4.1", - "ua-parser-js": "^1.0.33", - "yargs": "^17.3.1" - }, - "bin": { - "browser-sync": "dist/bin.js" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/browser-sync-client": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-2.29.3.tgz", - "integrity": "sha512-4tK5JKCl7v/3aLbmCBMzpufiYLsB1+UI+7tUXCCp5qF0AllHy/jAqYu6k7hUF3hYtlClKpxExWaR+rH+ny07wQ==", - "dev": true, - "dependencies": { - "etag": "1.8.1", - "fresh": "0.5.2", - "mitt": "^1.1.3" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/browser-sync-ui": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-2.29.3.tgz", - "integrity": "sha512-kBYOIQjU/D/3kYtUIJtj82e797Egk1FB2broqItkr3i4eF1qiHbFCG6srksu9gWhfmuM/TNG76jMfzAdxEPakg==", - "dev": true, - "dependencies": { - "async-each-series": "0.1.1", - "chalk": "4.1.2", - "connect-history-api-fallback": "^1", - "immutable": "^3", - "server-destroy": "1.0.1", - "socket.io-client": "^4.4.1", - "stream-throttle": "^0.1.3" - } - }, - "node_modules/browser-sync-ui/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/browser-sync-ui/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/browser-sync-ui/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/browser-sync-ui/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/browser-sync-ui/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/browser-sync-ui/node_modules/immutable": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", - "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/browser-sync-ui/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browser-sync/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/browser-sync/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/browser-sync/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/browser-sync/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/browser-sync/node_modules/fs-extra": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", - "integrity": "sha512-V3Z3WZWVUYd8hoCL5xfXJCaHWYzmtwW5XWYSlLgERi8PWd8bx1kUHUk8L1BT57e49oKnDDD180mjfrHc1yA9rg==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^3.0.0", - "universalify": "^0.1.0" - } - }, - "node_modules/browser-sync/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/browser-sync/node_modules/immutable": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", - "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/browser-sync/node_modules/jsonfile": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", - "integrity": "sha512-oBko6ZHlubVB5mRFkur5vgYR1UyqX+S6Y/oCfLhqNdcc2fYFlDpIoNc7AfKS1KOGcnNAkvsr0grLck9ANM815w==", - "dev": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/browser-sync/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browser-sync/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", "funding": [ { "type": "opencollective", @@ -6501,9 +5926,9 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, "bin": { @@ -6513,12 +5938,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/bs-recipes": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/bs-recipes/-/bs-recipes-1.3.4.tgz", - "integrity": "sha512-BXvDkqhDNxXEjeGM8LFkSbR+jzmP/CYpCiVKYn+soB1dDldeU15EBNDkwVXndKuX35wnNUaPd0qSoQEAkmQtMw==", - "dev": true - }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -6558,18 +5977,18 @@ } }, "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", "dev": true, "engines": { "node": ">= 0.8" } }, "node_modules/cacache": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.0.tgz", - "integrity": "sha512-I7mVOPl3PUCeRub1U8YoGz2Lqv9WOBpobZ8RyWFXmReuILz+3OAyTa5oH3QPdtKZD7N0Yk00aLfzn0qvp8dZ1w==", + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.2.tgz", + "integrity": "sha512-r3NU8h/P+4lVUHfeRw1dtgQYar3DZMm4/cm2bZgOvrFC/su7budSOeqh52VJIC4U4iG1WWwV6vRW0znqBvxNuw==", "dev": true, "dependencies": { "@npmcli/fs": "^3.1.0", @@ -6577,7 +5996,7 @@ "glob": "^10.2.2", "lru-cache": "^10.0.1", "minipass": "^7.0.3", - "minipass-collect": "^1.0.2", + "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^4.0.0", @@ -6590,23 +6009,14 @@ } }, "node_modules/cacache/node_modules/lru-cache": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", - "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", "dev": true, "engines": { "node": "14 || >=16.14" } }, - "node_modules/cacache/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/call-bind": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", @@ -6639,9 +6049,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001543", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001543.tgz", - "integrity": "sha512-qxdO8KPWPQ+Zk6bvNpPeQIOH47qZSYdFZd6dXQzb2KzhnSXju4Kd7H1PkSJx6NICSMgo/IhRZRhhfPTHYpJUCA==", + "version": "1.0.30001579", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz", + "integrity": "sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==", "funding": [ { "type": "opencollective", @@ -6874,15 +6284,6 @@ "node": ">= 0.8.0" } }, - "node_modules/compression/node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/compression/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -6910,45 +6311,15 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, - "node_modules/connect": { - "version": "3.6.6", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", - "integrity": "sha512-OO7axMmPpu/2XuX1+2Yrg0ddju31B6xLZMWkJ5rYBu4YRmRVlOjvlY6kw2FJKiAzyxGwnrDUAG4s1Pf0sbBMCQ==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "finalhandler": "1.1.0", - "parseurl": "~1.3.2", - "utils-merge": "1.0.1" - }, - "engines": { - "node": ">= 0.10.0" - } - }, "node_modules/connect-history-api-fallback": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", - "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", "dev": true, "engines": { "node": ">=0.8" } }, - "node_modules/connect/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/connect/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -6977,9 +6348,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", "dev": true, "engines": { "node": ">= 0.6" @@ -7071,12 +6442,12 @@ } }, "node_modules/core-js-compat": { - "version": "3.33.2", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.2.tgz", - "integrity": "sha512-axfo+wxFVxnqf8RvxTzoAlzW4gRoacrHeoFlc9n0x50+7BEyZL/Rt3hicaED1/CEd7I6tPCPVUYcJwCMO5XUYw==", + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz", + "integrity": "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==", "dev": true, "dependencies": { - "browserslist": "^4.22.1" + "browserslist": "^4.22.2" }, "funding": { "type": "opencollective", @@ -7089,27 +6460,14 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dev": true, - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/cosmiconfig": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.2.0.tgz", - "integrity": "sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==", + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "dependencies": { - "import-fresh": "^3.2.1", + "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", - "parse-json": "^5.0.0", + "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "engines": { @@ -7117,6 +6475,14 @@ }, "funding": { "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/cosmiconfig/node_modules/argparse": { @@ -7639,10 +7005,14 @@ } }, "node_modules/destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==", - "dev": true + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } }, "node_modules/detect-it": { "version": "4.0.1", @@ -7663,18 +7033,6 @@ "detect-it": "^4.0.1" } }, - "node_modules/dev-ip": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dev-ip/-/dev-ip-1.0.1.tgz", - "integrity": "sha512-LmVkry/oDShEgSZPNgqCIp2/TlqtExeGmymru3uCELnfyjY11IzpAproLYs+1X88fXO6DBoYP3ul2Xo2yz2j6A==", - "dev": true, - "bin": { - "dev-ip": "lib/dev-ip.js" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -7705,12 +7063,6 @@ "node": ">=8" } }, - "node_modules/dns-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", - "dev": true - }, "node_modules/dns-packet": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", @@ -7830,100 +7182,6 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, - "node_modules/easy-extender": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/easy-extender/-/easy-extender-2.3.4.tgz", - "integrity": "sha512-8cAwm6md1YTiPpOvDULYJL4ZS6WfM5/cTeVVh4JsvyYZAoqlRVUpHL9Gr5Fy7HA6xcSZicUia3DeAgO3Us8E+Q==", - "dev": true, - "dependencies": { - "lodash": "^4.17.10" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/eazy-logger": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/eazy-logger/-/eazy-logger-4.0.1.tgz", - "integrity": "sha512-2GSFtnnC6U4IEKhEI7+PvdxrmjJ04mdsj3wHZTFiw0tUtG4HCWzTr13ZYTk8XOGnA1xQMaDljoBOYlk3D/MMSw==", - "dev": true, - "dependencies": { - "chalk": "4.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/eazy-logger/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eazy-logger/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eazy-logger/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/eazy-logger/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/eazy-logger/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/eazy-logger/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -7946,9 +7204,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.540", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.540.tgz", - "integrity": "sha512-aoCqgU6r9+o9/S7wkcSbmPRFi7OWZWiXS9rtjEd+Ouyu/Xyw5RSq2XN8s5Qp8IaFOLiRrhQCphCIjAxgG3eCAg==" + "version": "1.4.640", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.640.tgz", + "integrity": "sha512-z/6oZ/Muqk4BaE7P69bXhUhpJbUM9ZJeka43ZwxsDshKtePns4mhBlh8bU5+yrnOnz3fhG82XLzGUXazOmsWnA==" }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -8005,91 +7263,6 @@ "once": "^1.4.0" } }, - "node_modules/engine.io": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.3.tgz", - "integrity": "sha512-IML/R4eG/pUS5w7OfcDE0jKrljWS9nwnEfsxWCIJF5eO6AHo6+Hlv+lQbdlAYsiJPHzUthLm1RUjnBzWOs45cw==", - "dev": true, - "dependencies": { - "@types/cookie": "^0.4.1", - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.4.1", - "cors": "~2.8.5", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.11.0" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/engine.io-client": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.2.tgz", - "integrity": "sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==", - "dev": true, - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.11.0", - "xmlhttprequest-ssl": "~2.0.0" - } - }, - "node_modules/engine.io-client/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", - "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/engine.io/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/enhanced-resolve": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", @@ -8170,9 +7343,9 @@ "dev": true }, "node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz", + "integrity": "sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==", "dev": true, "hasInstallScript": true, "bin": { @@ -8182,34 +7355,35 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" + "@esbuild/aix-ppc64": "0.19.11", + "@esbuild/android-arm": "0.19.11", + "@esbuild/android-arm64": "0.19.11", + "@esbuild/android-x64": "0.19.11", + "@esbuild/darwin-arm64": "0.19.11", + "@esbuild/darwin-x64": "0.19.11", + "@esbuild/freebsd-arm64": "0.19.11", + "@esbuild/freebsd-x64": "0.19.11", + "@esbuild/linux-arm": "0.19.11", + "@esbuild/linux-arm64": "0.19.11", + "@esbuild/linux-ia32": "0.19.11", + "@esbuild/linux-loong64": "0.19.11", + "@esbuild/linux-mips64el": "0.19.11", + "@esbuild/linux-ppc64": "0.19.11", + "@esbuild/linux-riscv64": "0.19.11", + "@esbuild/linux-s390x": "0.19.11", + "@esbuild/linux-x64": "0.19.11", + "@esbuild/netbsd-x64": "0.19.11", + "@esbuild/openbsd-x64": "0.19.11", + "@esbuild/sunos-x64": "0.19.11", + "@esbuild/win32-arm64": "0.19.11", + "@esbuild/win32-ia32": "0.19.11", + "@esbuild/win32-x64": "0.19.11" } }, "node_modules/esbuild-wasm": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.19.5.tgz", - "integrity": "sha512-7zmLLn2QCj93XfMmHtzrDJ1UBuOHB2CZz1ghoCEZiRajxjUvHsF40PnbzFIY/pmesqPRaEtEWii0uzsTbnAgrA==", + "version": "0.19.11", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.19.11.tgz", + "integrity": "sha512-MIhnpc1TxERUHomteO/ZZHp+kUawGEc03D/8vMHGzffLvbFLeDe6mwxqEZwlqBNY7SLWbyp6bBQAcCen8+wpjQ==", "dev": true, "bin": { "esbuild": "bin/esbuild" @@ -8634,12 +7808,6 @@ "node": ">=6" } }, - "node_modules/eventemitter-asyncresource": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz", - "integrity": "sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ==", - "dev": true - }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -8740,21 +7908,6 @@ "node": ">= 0.10.0" } }, - "node_modules/express/node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true - }, - "node_modules/express/node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -8764,106 +7917,12 @@ "ms": "2.0.0" } }, - "node_modules/express/node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/express/node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/express/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, - "node_modules/express/node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/express/node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/express/node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dev": true, - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/express/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -9051,17 +8110,17 @@ } }, "node_modules/finalhandler": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", - "integrity": "sha512-ejnvM9ZXYzp6PUPUyQBMBf0Co5VX2gr5H2VQe2Ui2jWXNlxv+PYZo8wpAymJNJdLsG1R4p+M4aynF8KuoUEwRw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", "dev": true, "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.1", + "encodeurl": "~1.0.2", "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "statuses": "~1.3.1", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", "unpipe": "~1.0.0" }, "engines": { @@ -9141,9 +8200,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", "dev": true, "funding": [ { @@ -9251,15 +8310,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/fs-monkey": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.4.tgz", @@ -9272,9 +8322,9 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, "optional": true, @@ -9564,9 +8614,9 @@ } }, "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", - "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", "dev": true, "engines": { "node": "14 || >=16.14" @@ -9683,15 +8733,6 @@ "node": ">= 0.8" } }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/http-parser-js": { "version": "0.5.8", "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", @@ -9824,9 +8865,9 @@ } }, "node_modules/ignore-walk": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.3.tgz", - "integrity": "sha512-C7FfFoTA+bI10qfeydT8aZbvr91vAEU+2W5BZUlzPec47oNb07SsOfwYrtxuvOYdUApPP/Qlh4DtAO51Ekk2QA==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.4.tgz", + "integrity": "sha512-t7sv42WkwFkyKbivUCglsQW5YWMskWtbEf4MNKX5u/CCWHKSPzN4FtBQGsQZgCLbxOzpVlcbWVK5KB3auIOjSw==", "dev": true, "dependencies": { "minimatch": "^9.0.0" @@ -9873,9 +8914,9 @@ } }, "node_modules/immutable": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", - "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", + "integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==", "dev": true }, "node_modules/import-fresh": { @@ -9943,12 +8984,12 @@ } }, "node_modules/inquirer": { - "version": "9.2.11", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.11.tgz", - "integrity": "sha512-B2LafrnnhbRzCWfAdOXisUzL89Kg8cVJlYmhqoi3flSiV/TveO+nsXwgKr9h9PIo+J1hz7nBSk6gegRIMBBf7g==", + "version": "9.2.12", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.12.tgz", + "integrity": "sha512-mg3Fh9g2zfuVWJn6lhST0O7x4n03k7G8Tx5nvikJkbq8/CK47WDVm+UznF0G6s5Zi0KcyUisr6DU8T67N5U+1Q==", "dev": true, "dependencies": { - "@ljharb/through": "^2.3.9", + "@ljharb/through": "^2.3.11", "ansi-escapes": "^4.3.2", "chalk": "^5.3.0", "cli-cursor": "^3.1.0", @@ -10143,15 +9184,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-number-like": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/is-number-like/-/is-number-like-1.0.8.tgz", - "integrity": "sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==", - "dev": true, - "dependencies": { - "lodash.isfinite": "^3.3.2" - } - }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -10589,9 +9621,9 @@ } }, "node_modules/jiti": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.20.0.tgz", - "integrity": "sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", "dev": true, "bin": { "jiti": "bin/jiti.js" @@ -10854,12 +9886,6 @@ } } }, - "node_modules/limiter": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", - "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==", - "dev": true - }, "node_modules/lines-and-columns": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.4.tgz", @@ -10887,79 +9913,6 @@ "node": ">= 12.13.0" } }, - "node_modules/localtunnel": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/localtunnel/-/localtunnel-2.0.2.tgz", - "integrity": "sha512-n418Cn5ynvJd7m/N1d9WVJISLJF/ellZnfsLnx8WBWGzxv/ntNcFkJ1o6se5quUhCplfLGBNL5tYHiq5WF3Nug==", - "dev": true, - "dependencies": { - "axios": "0.21.4", - "debug": "4.3.2", - "openurl": "1.1.1", - "yargs": "17.1.1" - }, - "bin": { - "lt": "bin/lt.js" - }, - "engines": { - "node": ">=8.3.0" - } - }, - "node_modules/localtunnel/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/localtunnel/node_modules/debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/localtunnel/node_modules/yargs": { - "version": "17.1.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.1.1.tgz", - "integrity": "sha512-c2k48R0PwKIqKhPMWjeiF6y2xY/gPMUlro0sgxqXpbOIohWiLNXWslsootttv7E1e73QPAMQSg5FeySbVcpsPQ==", - "dev": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/localtunnel/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -10989,12 +9942,6 @@ "resolved": "https://registry.npmjs.org/lodash.deburr/-/lodash.deburr-4.1.0.tgz", "integrity": "sha512-m/M1U1f3ddMCs6Hq2tAsYThTBDaAKFDX3dwDo97GEYzamXi9SqUpjWi/Rrj/gf3X2n8ktwgZrlP1z6E3v/IExQ==" }, - "node_modules/lodash.isfinite": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz", - "integrity": "sha512-7FGG40uhC8Mm633uKW1r58aElFlBlxCrg9JfSi3P6aYiWmfiWF0PgMd86ZUsxE5GwWPdHoS2+48bwTh2VPkIQA==", - "dev": true - }, "node_modules/lodash.kebabcase": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", @@ -11165,15 +10112,6 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/make-fetch-happen/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -11324,43 +10262,25 @@ } }, "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, "node_modules/minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", "dev": true, "dependencies": { - "minipass": "^3.0.0" + "minipass": "^7.0.3" }, "engines": { - "node": ">= 8" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/minipass-collect/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-collect/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/minipass-fetch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.4.tgz", @@ -11378,15 +10298,6 @@ "encoding": "^0.1.13" } }, - "node_modules/minipass-fetch/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/minipass-flush": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", @@ -11536,12 +10447,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/mitt": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-1.2.0.tgz", - "integrity": "sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==", - "dev": true - }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -11591,9 +10496,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true, "funding": [ { @@ -11697,29 +10602,17 @@ } }, "node_modules/ng-select2-component": { - "version": "13.0.9", - "resolved": "https://registry.npmjs.org/ng-select2-component/-/ng-select2-component-13.0.9.tgz", - "integrity": "sha512-Bj7lHCiHnwNFECyzpn0LyD3IOPnBbIHHYXxpFU313QZgVkEz7oiF9nBnkorAAABIfLk4EiU0nBQkY3CmbVOgfg==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/ng-select2-component/-/ng-select2-component-14.0.0.tgz", + "integrity": "sha512-AYfF3Hyc+RtryoWIpBVBOBqjF6wKFB6KfzYVrtENEqv6W3qDRRQDfm17CFXm1UZxgoprlsYOeefo4aD4R06s5g==", "dependencies": { - "ngx-infinite-scroll": ">=16.0.0", + "ngx-infinite-scroll": ">=17.0.0", "tslib": "^2.3.0" }, "peerDependencies": { - "@angular/cdk": ">=16.1.0", - "@angular/common": ">=16.1.0", - "@angular/core": ">=16.1.0" - } - }, - "node_modules/ng-select2-component/node_modules/ngx-infinite-scroll": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/ngx-infinite-scroll/-/ngx-infinite-scroll-16.0.0.tgz", - "integrity": "sha512-bzyNYd+wVlUUxcopRVr2DAa81eEc8vITtKVvb+c7R1uy8hWPTlxOEXf3L1qA4FMwTEzCQ9b37TXzlJji3qBy+A==", - "dependencies": { - "tslib": "^2.3.0" - }, - "peerDependencies": { - "@angular/common": ">=16.0.0 <17.0.0", - "@angular/core": ">=16.0.0 <17.0.0" + "@angular/cdk": ">=17.0.0", + "@angular/common": ">=17.0.0", + "@angular/core": ">=17.0.0" } }, "node_modules/ngx-color-picker": { @@ -11736,9 +10629,9 @@ } }, "node_modules/ngx-extended-pdf-viewer": { - "version": "18.1.9", - "resolved": "https://registry.npmjs.org/ngx-extended-pdf-viewer/-/ngx-extended-pdf-viewer-18.1.9.tgz", - "integrity": "sha512-puISS6h1JoHObo0BZK68EhlWlI215AWP5RJ5D86yuWiBgVYeNUa8JrEVnaJtQ/bI6WbfvleaBe8NBwKnM0Bqsw==", + "version": "18.1.14", + "resolved": "https://registry.npmjs.org/ngx-extended-pdf-viewer/-/ngx-extended-pdf-viewer-18.1.14.tgz", + "integrity": "sha512-8nz0sQWPn3BrN8rLy0vHrORZ3FJWPKDBt2eOJANxTmEKr0mkVECHqOoK47EfZMrc/+zwCzJkzTskA9w3CzJM/A==", "dependencies": { "lodash.deburr": "^4.1.0", "tslib": "^2.3.0" @@ -11764,6 +10657,18 @@ "@angular/core": ">=14.0.0" } }, + "node_modules/ngx-infinite-scroll": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/ngx-infinite-scroll/-/ngx-infinite-scroll-17.0.0.tgz", + "integrity": "sha512-pQXLuRiuhRuDKD3nmgyW1V08JVNBepmk6nb8qjHc5hgsWNts01+R/p33rYcRDzcut6/PWqGyrZ9o9i8swzMYMA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=17.0.0 <18.0.0", + "@angular/core": ">=17.0.0 <18.0.0" + } + }, "node_modules/ngx-slider-v2": { "version": "17.0.0", "resolved": "https://registry.npmjs.org/ngx-slider-v2/-/ngx-slider-v2-17.0.0.tgz", @@ -11892,9 +10797,9 @@ } }, "node_modules/node-gyp-build": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.1.tgz", - "integrity": "sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", + "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", "dev": true, "optional": true, "bin": { @@ -11934,9 +10839,9 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, "node_modules/nopt": { "version": "7.2.0", @@ -12040,12 +10945,12 @@ } }, "node_modules/npm-packlist": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.0.tgz", - "integrity": "sha512-ErAGFB5kJUciPy1mmx/C2YFbvxoJ0QJ9uwkCZOeR6CqLLISPZBOiFModAbSXnjjlwW5lOhuhXva+fURsSGJqyw==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.2.tgz", + "integrity": "sha512-shYrPFIS/JLP4oQmAwDyk5HcyysKW8/JLTEA32S0Z5TzvpaeeX2yMFfoK1fjEBnCBvVyIB/Jj/GBFdm0wsgzbA==", "dev": true, "dependencies": { - "ignore-walk": "^6.0.0" + "ignore-walk": "^6.0.4" }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -12084,15 +10989,6 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/npm-registry-fetch/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -12118,13 +11014,13 @@ } }, "node_modules/nx": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/nx/-/nx-17.1.3.tgz", - "integrity": "sha512-6LYoTt01nS1d/dvvYtRs+pEAMQmUVsd2fr/a8+X1cDjWrb8wsf1O3DwlBTqKOXOazpS3eOr0Ukc9N1svbu7uXA==", + "version": "17.2.8", + "resolved": "https://registry.npmjs.org/nx/-/nx-17.2.8.tgz", + "integrity": "sha512-rM5zXbuXLEuqQqcjVjClyvHwRJwt+NVImR2A6KFNG40Z60HP6X12wAxxeLHF5kXXTDRU0PFhf/yACibrpbPrAw==", "dev": true, "hasInstallScript": true, "dependencies": { - "@nrwl/tao": "17.1.3", + "@nrwl/tao": "17.2.8", "@yarnpkg/lockfile": "^1.1.0", "@yarnpkg/parsers": "3.0.0-rc.46", "@zkochan/js-yaml": "0.0.6", @@ -12156,7 +11052,6 @@ "tmp": "~0.2.1", "tsconfig-paths": "^4.1.2", "tslib": "^2.3.0", - "v8-compile-cache": "2.3.0", "yargs": "^17.6.2", "yargs-parser": "21.1.1" }, @@ -12165,16 +11060,16 @@ "nx-cloud": "bin/nx-cloud.js" }, "optionalDependencies": { - "@nx/nx-darwin-arm64": "17.1.3", - "@nx/nx-darwin-x64": "17.1.3", - "@nx/nx-freebsd-x64": "17.1.3", - "@nx/nx-linux-arm-gnueabihf": "17.1.3", - "@nx/nx-linux-arm64-gnu": "17.1.3", - "@nx/nx-linux-arm64-musl": "17.1.3", - "@nx/nx-linux-x64-gnu": "17.1.3", - "@nx/nx-linux-x64-musl": "17.1.3", - "@nx/nx-win32-arm64-msvc": "17.1.3", - "@nx/nx-win32-x64-msvc": "17.1.3" + "@nx/nx-darwin-arm64": "17.2.8", + "@nx/nx-darwin-x64": "17.2.8", + "@nx/nx-freebsd-x64": "17.2.8", + "@nx/nx-linux-arm-gnueabihf": "17.2.8", + "@nx/nx-linux-arm64-gnu": "17.2.8", + "@nx/nx-linux-arm64-musl": "17.2.8", + "@nx/nx-linux-x64-gnu": "17.2.8", + "@nx/nx-linux-x64-musl": "17.2.8", + "@nx/nx-win32-arm64-msvc": "17.2.8", + "@nx/nx-win32-x64-msvc": "17.2.8" }, "peerDependencies": { "@swc-node/register": "^1.6.7", @@ -12211,12 +11106,12 @@ "dev": true }, "node_modules/nx/node_modules/axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", + "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", "dev": true, "dependencies": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.4", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -12308,6 +11203,18 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/nx/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/nx/node_modules/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.5.tgz", @@ -12320,6 +11227,21 @@ "node": "*" } }, + "node_modules/nx/node_modules/semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/nx/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -12332,14 +11254,11 @@ "node": ">=8" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } + "node_modules/nx/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/object-inspect": { "version": "1.13.1", @@ -12357,9 +11276,9 @@ "dev": true }, "node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "dev": true, "dependencies": { "ee-first": "1.1.1" @@ -12425,33 +11344,6 @@ "opener": "bin/opener-bin.js" } }, - "node_modules/openurl": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/openurl/-/openurl-1.1.1.tgz", - "integrity": "sha512-d/gTkTb1i1GKz5k3XE3XFV/PxQ1k45zDqGP2OA7YhgsaLoqm6qRvARAZOFer1fcXritWlGBRCu/UgeS4HAnXAA==", - "dev": true - }, - "node_modules/opn": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz", - "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==", - "dev": true, - "dependencies": { - "is-wsl": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/opn/node_modules/is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -12638,9 +11530,9 @@ } }, "node_modules/pacote": { - "version": "17.0.4", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-17.0.4.tgz", - "integrity": "sha512-eGdLHrV/g5b5MtD5cTPyss+JxOlaOloSMG3UwPMAvL8ywaLJ6beONPF40K4KKl/UI6q5hTKCJq5rCu8tkF+7Dg==", + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-17.0.5.tgz", + "integrity": "sha512-TAE0m20zSDMnchPja9vtQjri19X3pZIyRpm2TJVeI+yU42leJBBDTRYhOcWFsPhaMxf+3iwQkFiKz16G9AEeeA==", "dev": true, "dependencies": { "@npmcli/git": "^5.0.0", @@ -12669,15 +11561,6 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/pacote/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -12869,12 +11752,11 @@ } }, "node_modules/piscina": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.1.0.tgz", - "integrity": "sha512-sjbLMi3sokkie+qmtZpkfMCUJTpbxJm/wvaPzU28vmYSsTSW8xk9JcFUsbqGJdtPpIQ9tuj+iDcTtgZjwnOSig==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.2.1.tgz", + "integrity": "sha512-LShp0+lrO+WIzB9LXO+ZmO4zGHxtTJNZhEO56H9SSu+JPaUQb6oLcTCzWi5IL2DS8/vIkCE88ElahuSSw4TAkA==", "dev": true, "dependencies": { - "eventemitter-asyncresource": "^1.0.0", "hdr-histogram-js": "^2.0.1", "hdr-histogram-percentiles-obj": "^3.0.0" }, @@ -12979,33 +11861,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/portscanner": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.2.0.tgz", - "integrity": "sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw==", - "dev": true, - "dependencies": { - "async": "^2.6.0", - "is-number-like": "^1.0.3" - }, - "engines": { - "node": ">=0.4", - "npm": ">=1.0.0" - } - }, - "node_modules/portscanner/node_modules/async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "dev": true, - "dependencies": { - "lodash": "^4.17.14" - } - }, "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "version": "8.4.33", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", + "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", "dev": true, "funding": [ { @@ -13022,7 +11881,7 @@ } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -13031,14 +11890,14 @@ } }, "node_modules/postcss-loader": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.3.tgz", - "integrity": "sha512-YgO/yhtevGO/vJePCQmTxiaEwER94LABZN0ZMT4A0vsak9TpO+RvKRs7EmJ8peIlB9xfXCsS7M8LjqncsUZ5HA==", + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz", + "integrity": "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==", "dev": true, "dependencies": { - "cosmiconfig": "^8.2.0", - "jiti": "^1.18.2", - "semver": "^7.3.8" + "cosmiconfig": "^8.3.5", + "jiti": "^1.20.0", + "semver": "^7.5.4" }, "engines": { "node": ">= 14.15.0" @@ -13317,9 +12176,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", "dev": true, "dependencies": { "bytes": "3.1.2", @@ -13331,6 +12190,15 @@ "node": ">= 0.8" } }, + "node_modules/raw-body/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -13366,18 +12234,18 @@ } }, "node_modules/read-package-json-fast/node_modules/json-parse-even-better-errors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", - "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz", + "integrity": "sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==", "dev": true, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/read-package-json/node_modules/json-parse-even-better-errors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", - "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz", + "integrity": "sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==", "dev": true, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -13433,9 +12301,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "dev": true }, "node_modules/regenerator-transform": { @@ -13695,34 +12563,6 @@ "node": ">=0.10.0" } }, - "node_modules/resp-modifier": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/resp-modifier/-/resp-modifier-6.0.2.tgz", - "integrity": "sha512-U1+0kWC/+4ncRFYqQWTx/3qkfE6a4B/h3XXgmXypfa0SPZ3t7cbbaFk297PjQS/yov24R18h6OZe6iZwj3NSLw==", - "dev": true, - "dependencies": { - "debug": "^2.2.0", - "minimatch": "^3.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/resp-modifier/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/resp-modifier/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -13799,18 +12639,34 @@ } }, "node_modules/rollup": { - "version": "3.29.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", - "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.6.tgz", + "integrity": "sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==", "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, "bin": { "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=14.18.0", + "node": ">=18.0.0", "npm": ">=8.0.0" }, "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.9.6", + "@rollup/rollup-android-arm64": "4.9.6", + "@rollup/rollup-darwin-arm64": "4.9.6", + "@rollup/rollup-darwin-x64": "4.9.6", + "@rollup/rollup-linux-arm-gnueabihf": "4.9.6", + "@rollup/rollup-linux-arm64-gnu": "4.9.6", + "@rollup/rollup-linux-arm64-musl": "4.9.6", + "@rollup/rollup-linux-riscv64-gnu": "4.9.6", + "@rollup/rollup-linux-x64-gnu": "4.9.6", + "@rollup/rollup-linux-x64-musl": "4.9.6", + "@rollup/rollup-win32-arm64-msvc": "4.9.6", + "@rollup/rollup-win32-ia32-msvc": "4.9.6", + "@rollup/rollup-win32-x64-msvc": "4.9.6", "fsevents": "~2.3.2" } }, @@ -13845,12 +12701,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/rx": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", - "integrity": "sha512-CiaiuN6gapkdl+cZUr67W6I8jquN4lkak3vtIsIWCl4XIPP8ffsoyN6/+PuGXnQy8Cu8W2y9Xxh31Rq4M6wUug==", - "dev": true - }, "node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -13885,9 +12735,9 @@ "dev": true }, "node_modules/sass": { - "version": "1.69.5", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz", - "integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==", + "version": "1.69.7", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.7.tgz", + "integrity": "sha512-rzj2soDeZ8wtE2egyLXgOOHQvaC2iosZrkF6v3EUG+tBwEvhqUCzm0VP3k9gHF9LXbSrRhT5SksoI56Iw8NPnQ==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -13902,9 +12752,9 @@ } }, "node_modules/sass-loader": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.2.tgz", - "integrity": "sha512-CQbKl57kdEv+KDLquhC+gE3pXt74LEAzm+tzywcA0/aHZuub8wTErbjAoNI57rPUWRYRNC5WUnNl8eGJNbDdwg==", + "version": "13.3.3", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.3.tgz", + "integrity": "sha512-mt5YN2F1MOZr3d/wBRcZxeFgwgkH44wVc2zohO2YF6JiOMkiXe4BYRZpSu2sO1g71mo/j16txzUhsKZlqjVGzA==", "dev": true, "dependencies": { "neo-async": "^2.6.2" @@ -13995,9 +12845,9 @@ } }, "node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -14028,24 +12878,24 @@ "dev": true }, "node_modules/send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", "dev": true, "dependencies": { "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", + "depd": "2.0.0", + "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", - "http-errors": "~1.6.2", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "~2.3.0", - "range-parser": "~1.2.0", - "statuses": "~1.4.0" + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" }, "engines": { "node": ">= 0.8.0" @@ -14060,66 +12910,18 @@ "ms": "2.0.0" } }, - "node_modules/send/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", - "dev": true, - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "dev": true - }, - "node_modules/send/node_modules/mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", - "dev": true, - "bin": { - "mime": "cli.js" - } - }, - "node_modules/send/node_modules/ms": { + "node_modules/send/node_modules/debug/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, - "node_modules/send/node_modules/setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "node_modules/send/node_modules/statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/serialize-javascript": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", @@ -14208,41 +13010,36 @@ } }, "node_modules/serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", "dev": true, "dependencies": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", - "parseurl": "~1.3.2", - "send": "0.16.2" + "parseurl": "~1.3.3", + "send": "0.18.0" }, "engines": { "node": ">= 0.8.0" } }, - "node_modules/server-destroy": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", - "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", - "dev": true - }, "node_modules/set-cookie-parser": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" }, "node_modules/set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", + "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", "dev": true, "dependencies": { "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.2", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -14320,15 +13117,17 @@ } }, "node_modules/sigstore": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.1.0.tgz", - "integrity": "sha512-kPIj+ZLkyI3QaM0qX8V/nSsweYND3W448pwkDgS6CQ74MfhEkIR8ToK5Iyx46KJYRjseVcD3Rp9zAmUAj6ZjPw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.2.0.tgz", + "integrity": "sha512-fcU9clHwEss2/M/11FFM8Jwc4PjBgbhXoNskoK5guoK0qGQBSeUbQZRJ+B2fDFIvhyf0gqCaPrel9mszbhAxug==", "dev": true, "dependencies": { - "@sigstore/bundle": "^2.1.0", + "@sigstore/bundle": "^2.1.1", + "@sigstore/core": "^0.2.0", "@sigstore/protobuf-specs": "^0.2.1", - "@sigstore/sign": "^2.1.0", - "@sigstore/tuf": "^2.1.0" + "@sigstore/sign": "^2.2.1", + "@sigstore/tuf": "^2.3.0", + "@sigstore/verify": "^0.1.0" }, "engines": { "node": "^16.14.0 || >=18.0.0" @@ -14367,82 +13166,6 @@ "npm": ">= 3.0.0" } }, - "node_modules/socket.io": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz", - "integrity": "sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==", - "dev": true, - "dependencies": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "cors": "~2.8.5", - "debug": "~4.3.2", - "engine.io": "~6.5.2", - "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/socket.io-adapter": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", - "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", - "dev": true, - "dependencies": { - "ws": "~8.11.0" - } - }, - "node_modules/socket.io-adapter/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/socket.io-client": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz", - "integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==", - "dev": true, - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", - "dev": true, - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -14501,17 +13224,16 @@ } }, "node_modules/source-map-loader": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-4.0.1.tgz", - "integrity": "sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", "dev": true, "dependencies": { - "abab": "^2.0.6", "iconv-lite": "^0.6.3", "source-map-js": "^1.0.2" }, "engines": { - "node": ">= 14.15.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", @@ -14637,38 +13359,13 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/ssri/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/statuses": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha512-wuTCPGlJONk/a1kqZ4fQM2+908lC7fa7nPYpTC1EhnvqLX/IICbeP1OZGDtA374trpSq68YubKUMo8oRhN46yg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "dev": true, "engines": { - "node": ">= 0.6" - } - }, - "node_modules/stream-throttle": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/stream-throttle/-/stream-throttle-0.1.3.tgz", - "integrity": "sha512-889+B9vN9dq7/vLbGyuHeZ6/ctf5sNuGWsDy89uNxkFTAgzy0eK7+w5fL3KLNRTkLle7EgZGvHUphZW0Q26MnQ==", - "dev": true, - "dependencies": { - "commander": "^2.2.0", - "limiter": "^1.0.5" - }, - "bin": { - "throttleproxy": "bin/throttleproxy.js" - }, - "engines": { - "node": ">= 0.10.0" + "node": ">= 0.8" } }, "node_modules/string_decoder": { @@ -14897,6 +13594,15 @@ "node": ">=8" } }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/tar/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -14904,9 +13610,9 @@ "dev": true }, "node_modules/terser": { - "version": "5.24.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz", - "integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==", + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.26.0.tgz", + "integrity": "sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -15216,9 +13922,9 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tuf-js": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.1.0.tgz", - "integrity": "sha512-eD7YPPjVlMzdggrOeE8zwoegUaG/rt6Bt3jwoQPunRiNVzgcCE009UDFJKJjG+Gk9wFu6W/Vi+P5d/5QpdD9jA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.2.0.tgz", + "integrity": "sha512-ZSDngmP1z6zw+FIkIBjvOp/II/mIub/O7Pp12j1WNsiCpg5R5wAc//i555bBQsE44O94btLt0xM/Zr2LQjwdCg==", "dev": true, "dependencies": { "@tufjs/models": "2.0.0", @@ -15285,39 +13991,16 @@ "node": ">=14.17" } }, - "node_modules/ua-parser-js": { - "version": "1.0.37", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz", - "integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } - ], - "engines": { - "node": "*" - } - }, "node_modules/undici": { - "version": "5.27.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.27.2.tgz", - "integrity": "sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.2.1.tgz", + "integrity": "sha512-7Wa9thEM6/LMnnKtxJHlc8SrTlDmxqJecgz1iy8KlsN0/iskQXOQCuPkrZLXbElPaSw5slFFyKIKXyJ3UtbApw==", "dev": true, "dependencies": { "@fastify/busboy": "^2.0.0" }, "engines": { - "node": ">=14.0" + "node": ">=18.0" } }, "node_modules/undici-types": { @@ -15477,12 +14160,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -15521,29 +14198,29 @@ } }, "node_modules/vite": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz", - "integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==", + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.11.tgz", + "integrity": "sha512-XBMnDjZcNAw/G1gEiskiM1v6yzM4GE5aMGvhWTlHAYYhxb7S3/V1s3m2LDHa8Vh6yIWYYB0iJwsEaS523c4oYA==", "dev": true, "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.27", - "rollup": "^3.27.1" + "esbuild": "^0.19.3", + "postcss": "^8.4.32", + "rollup": "^4.2.0" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^18.0.0 || >=20.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" }, "optionalDependencies": { - "fsevents": "~2.3.2" + "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": ">= 14", + "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", @@ -15801,15 +14478,6 @@ } } }, - "node_modules/webpack-dev-server/node_modules/connect-history-api-fallback": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", - "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", @@ -15834,9 +14502,9 @@ } }, "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.15.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.15.1.tgz", - "integrity": "sha512-W5OZiCjXEmk0yZ66ZN82beM5Sz7l7coYxpRkzS+p9PP+ToQry8szKh+61eNktr7EA9DOwvFGhfC605jDHbP6QQ==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", "dev": true, "engines": { "node": ">=10.0.0" @@ -16139,15 +14807,6 @@ } } }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", - "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/UI/Web/package.json b/UI/Web/package.json index 1ccc7a248..011146c69 100644 --- a/UI/Web/package.json +++ b/UI/Web/package.json @@ -1,29 +1,31 @@ { "name": "kavita-webui", - "version": "0.4.2", + "version": "0.7.12.1", "scripts": { "ng": "ng", - "start": "ng serve", - "build": "ng build", + "start": "npm run cache-locale && ng serve", + "build": "npm run cache-locale && ng build", "minify-langs": "node minify-json.js", - "prod": "ng build --configuration production --aot --output-hashing=all && npm run minify-langs", + "cache-locale": "node hash-localization.js", + "cache-locale-prime": "node hash-localization-prime.js", + "prod": "npm run cache-locale-prime && ng build --configuration production && npm run minify-langs && npm run cache-locale", "explore": "ng build --stats-json && webpack-bundle-analyzer dist/stats.json", "lint": "ng lint", "e2e": "ng e2e" }, "private": true, "dependencies": { - "@angular/animations": "^17.0.6", - "@angular/cdk": "^17.0.4", - "@angular/common": "^17.0.6", - "@angular/compiler": "^17.0.6", - "@angular/core": "^17.0.6", - "@angular/forms": "^17.0.6", - "@angular/localize": "^17.0.6", - "@angular/platform-browser": "^17.0.6", - "@angular/platform-browser-dynamic": "^17.0.6", - "@angular/router": "^17.0.6", - "@fortawesome/fontawesome-free": "^6.4.2", + "@angular/animations": "^17.1.0", + "@angular/cdk": "^17.1.0", + "@angular/common": "^17.1.0", + "@angular/compiler": "^17.1.0", + "@angular/core": "^17.1.0", + "@angular/forms": "^17.1.0", + "@angular/localize": "^17.1.0", + "@angular/platform-browser": "^17.1.0", + "@angular/platform-browser-dynamic": "^17.1.0", + "@angular/router": "^17.1.0", + "@fortawesome/fontawesome-free": "^6.5.1", "@iharbeck/ngx-virtual-scroller": "^17.0.0", "@iplab/ngx-file-upload": "^17.0.0", "@microsoft/signalr": "^7.0.12", @@ -42,7 +44,7 @@ "luxon": "^3.4.4", "ng-circle-progress": "^1.7.1", "ng-lazyload-image": "^9.1.3", - "ng-select2-component": "^13.0.9", + "ng-select2-component": "^14.0.0", "ngx-color-picker": "^16.0.0", "ngx-extended-pdf-viewer": "^18.1.9", "ngx-file-drop": "^16.0.0", @@ -58,20 +60,20 @@ "zone.js": "^0.14.2" }, "devDependencies": { - "@angular-devkit/build-angular": "^17.0.7", - "@angular-eslint/builder": "^17.1.1", - "@angular-eslint/eslint-plugin": "^17.1.1", - "@angular-eslint/eslint-plugin-template": "^17.1.1", - "@angular-eslint/schematics": "^17.1.1", - "@angular-eslint/template-parser": "^17.1.1", - "@angular/cli": "^17.0.7", - "@angular/compiler-cli": "^17.0.6", + "@angular-devkit/build-angular": "^17.1.0", + "@angular-eslint/builder": "^17.2.1", + "@angular-eslint/eslint-plugin": "^17.2.1", + "@angular-eslint/eslint-plugin-template": "^17.2.1", + "@angular-eslint/schematics": "^17.2.1", + "@angular-eslint/template-parser": "^17.2.1", + "@angular/cli": "^17.1.0", + "@angular/compiler-cli": "^17.1.0", "@types/d3": "^7.4.3", "@types/file-saver": "^2.0.7", - "@types/luxon": "^3.3.7", + "@types/luxon": "^3.4.0", "@types/node": "^20.10.0", "@typescript-eslint/eslint-plugin": "^6.13.0", - "@typescript-eslint/parser": "^6.13.0", + "@typescript-eslint/parser": "^6.19.0", "eslint": "^8.54.0", "jsonminify": "^0.4.2", "karma-coverage": "~2.2.0", diff --git a/UI/Web/src/app/_models/events/update-version-event.ts b/UI/Web/src/app/_models/events/update-version-event.ts index d5845881c..c74e49af6 100644 --- a/UI/Web/src/app/_models/events/update-version-event.ts +++ b/UI/Web/src/app/_models/events/update-version-event.ts @@ -6,4 +6,7 @@ export interface UpdateVersionEvent { updateUrl: string; isDocker: boolean; publishDate: string; -} \ No newline at end of file + isOnNightlyInRelease: boolean; + isReleaseNewer: boolean; + isReleaseEqual: boolean; +} diff --git a/UI/Web/src/app/_models/library/library.ts b/UI/Web/src/app/_models/library/library.ts index c42117af2..30ee14819 100644 --- a/UI/Web/src/app/_models/library/library.ts +++ b/UI/Web/src/app/_models/library/library.ts @@ -5,7 +5,9 @@ export enum LibraryType { Comic = 1, Book = 2, Images = 3, - Magazine = 4 + Images = 3, + LightNovel = 4, + Magazine = 5 } export interface Library { diff --git a/UI/Web/src/app/_models/metadata/series-filter.ts b/UI/Web/src/app/_models/metadata/series-filter.ts index 843844416..663fc2380 100644 --- a/UI/Web/src/app/_models/metadata/series-filter.ts +++ b/UI/Web/src/app/_models/metadata/series-filter.ts @@ -21,6 +21,10 @@ export enum SortField { TimeToRead = 5, ReleaseYear = 6, ReadProgress = 7, + /** + * Kavita+ only + */ + AverageRating = 8 } export const allSortFields = Object.keys(SortField) diff --git a/UI/Web/src/app/_models/metadata/v2/filter-field.ts b/UI/Web/src/app/_models/metadata/v2/filter-field.ts index 8544e5ccb..76c44b01c 100644 --- a/UI/Web/src/app/_models/metadata/v2/filter-field.ts +++ b/UI/Web/src/app/_models/metadata/v2/filter-field.ts @@ -28,7 +28,8 @@ export enum FilterField Path = 24, FilePath = 25, WantToRead = 26, - ReadingDate = 27 + ReadingDate = 27, + AverageRating = 28 } diff --git a/UI/Web/src/app/_models/scrobbling/scrobble-event.ts b/UI/Web/src/app/_models/scrobbling/scrobble-event.ts index 41113e5d0..48a75afda 100644 --- a/UI/Web/src/app/_models/scrobbling/scrobble-event.ts +++ b/UI/Web/src/app/_models/scrobbling/scrobble-event.ts @@ -18,4 +18,10 @@ export interface ScrobbleEvent { createdUtc: string; volumeNumber: number | null; chapterNumber: number | null; + isErrored: boolean; + /** + * Null when not errored + */ + errorDetails: string | null; + } diff --git a/UI/Web/src/app/_models/series-detail/series-detail-plus.ts b/UI/Web/src/app/_models/series-detail/series-detail-plus.ts new file mode 100644 index 000000000..8160b210f --- /dev/null +++ b/UI/Web/src/app/_models/series-detail/series-detail-plus.ts @@ -0,0 +1,9 @@ +import {Recommendation} from "./recommendation"; +import {UserReview} from "../../_single-module/review-card/user-review"; +import {Rating} from "../rating"; + +export interface SeriesDetailPlus { + recommendations?: Recommendation; + reviews: Array; + ratings?: Array; +} diff --git a/UI/Web/src/app/_models/volume.ts b/UI/Web/src/app/_models/volume.ts index 48da5a510..e944438a3 100644 --- a/UI/Web/src/app/_models/volume.ts +++ b/UI/Web/src/app/_models/volume.ts @@ -3,7 +3,8 @@ import { HourEstimateRange } from './series-detail/hour-estimate-range'; export interface Volume { id: number; - number: number; + minNumber: number; + maxNumber: number; name: string; createdUtc: string; lastModifiedUtc: string; diff --git a/UI/Web/src/app/_pipes/filter-field.pipe.ts b/UI/Web/src/app/_pipes/filter-field.pipe.ts index 10e4399bc..d94960dce 100644 --- a/UI/Web/src/app/_pipes/filter-field.pipe.ts +++ b/UI/Web/src/app/_pipes/filter-field.pipe.ts @@ -64,8 +64,10 @@ export class FilterFieldPipe implements PipeTransform { return translate('filter-field-pipe.file-path'); case FilterField.WantToRead: return translate('filter-field-pipe.want-to-read'); - case FilterField.ReadingDate: + case FilterField.ReadingDate: return translate('filter-field-pipe.read-date'); + case FilterField.AverageRating: + return translate('filter-field-pipe.average-rating'); default: throw new Error(`Invalid FilterField value: ${value}`); } diff --git a/UI/Web/src/app/_pipes/sort-field.pipe.ts b/UI/Web/src/app/_pipes/sort-field.pipe.ts index 1fe878860..ea54d124d 100644 --- a/UI/Web/src/app/_pipes/sort-field.pipe.ts +++ b/UI/Web/src/app/_pipes/sort-field.pipe.ts @@ -27,6 +27,8 @@ export class SortFieldPipe implements PipeTransform { return this.translocoService.translate('sort-field-pipe.release-year'); case SortField.ReadProgress: return this.translocoService.translate('sort-field-pipe.read-progress'); + case SortField.AverageRating: + return this.translocoService.translate('sort-field-pipe.average-rating'); } } diff --git a/UI/Web/src/app/_services/account.service.ts b/UI/Web/src/app/_services/account.service.ts index 23e377411..a0ed0e20a 100644 --- a/UI/Web/src/app/_services/account.service.ts +++ b/UI/Web/src/app/_services/account.service.ts @@ -20,7 +20,8 @@ export enum Role { ChangePassword = 'Change Password', Bookmark = 'Bookmark', Download = 'Download', - ChangeRestriction = 'Change Restriction' + ChangeRestriction = 'Change Restriction', + ReadOnly = 'Read Only' } @Injectable({ @@ -80,6 +81,10 @@ export class AccountService { return user && user.roles.includes(Role.Bookmark); } + hasReadOnlyRole(user: User) { + return user && user.roles.includes(Role.ReadOnly); + } + getRoles() { return this.httpClient.get(this.baseUrl + 'account/roles'); } @@ -199,7 +204,7 @@ export class AccountService { } resendConfirmationEmail(userId: number) { - return this.httpClient.post(this.baseUrl + 'account/resend-confirmation-email?userId=' + userId, {}, TextResonse); + return this.httpClient.post(this.baseUrl + 'account/resend-confirmation-email?userId=' + userId, {}); } inviteUser(model: {email: string, roles: Array, libraries: Array, ageRestriction: AgeRestriction}) { @@ -310,7 +315,7 @@ export class AccountService { } - private refreshAccount() { + refreshAccount() { if (this.currentUser === null || this.currentUser === undefined) return of(); return this.httpClient.get(this.baseUrl + 'account/refresh-account').pipe(map((user: User) => { if (user) { diff --git a/UI/Web/src/app/_services/action-factory.service.ts b/UI/Web/src/app/_services/action-factory.service.ts index 15ead6554..645fd250c 100644 --- a/UI/Web/src/app/_services/action-factory.service.ts +++ b/UI/Web/src/app/_services/action-factory.service.ts @@ -237,6 +237,13 @@ export class ActionFactoryService { requiresAdmin: true, children: [], }, + { + action: Action.Delete, + title: 'delete', + callback: this.dummyCallback, + requiresAdmin: true, + children: [], + }, ], }, { diff --git a/UI/Web/src/app/_services/action.service.ts b/UI/Web/src/app/_services/action.service.ts index fa80a98c8..6e0e6448e 100644 --- a/UI/Web/src/app/_services/action.service.ts +++ b/UI/Web/src/app/_services/action.service.ts @@ -135,6 +135,26 @@ export class ActionService implements OnDestroy { }); } + async deleteLibrary(library: Partial, callback?: LibraryActionCallback) { + if (!library.hasOwnProperty('id') || library.id === undefined) { + return; + } + + if (!await this.confirmService.alert(translate('toasts.confirm-library-delete'))) { + if (callback) { + callback(library); + } + return; + } + + this.libraryService.delete(library?.id).pipe(take(1)).subscribe((res: any) => { + this.toastr.info(translate('toasts.library-deleted', {name: library.name})); + if (callback) { + callback(library); + } + }); + } + /** * Mark a series as read; updates the series pagesRead * @param series Series, must have id and name populated @@ -532,11 +552,9 @@ export class ActionService implements OnDestroy { } /** - * Mark all chapters and the volumes as Read. All volumes and chapters must belong to a series - * @param seriesId Series Id - * @param volumes Volumes, should have id, chapters and pagesRead populated - * @param chapters? Chapters, should have id - * @param callback Optional callback to perform actions after API completes + * Deletes all series + * @param seriesIds - List of series + * @param callback - Optional callback once complete */ async deleteMultipleSeries(seriesIds: Array, callback?: BooleanActionCallback) { if (!await this.confirmService.confirm(translate('toasts.confirm-delete-multiple-series', {count: seriesIds.length}))) { @@ -545,11 +563,15 @@ export class ActionService implements OnDestroy { } return; } - this.seriesService.deleteMultipleSeries(seriesIds.map(s => s.id)).pipe(take(1)).subscribe(() => { - this.toastr.success(translate('toasts.series-deleted')); + this.seriesService.deleteMultipleSeries(seriesIds.map(s => s.id)).pipe(take(1)).subscribe(res => { + if (res) { + this.toastr.success(translate('toasts.series-deleted')); + } else { + this.toastr.error(translate('errors.generic')); + } if (callback) { - callback(true); + callback(res); } }); } @@ -564,7 +586,12 @@ export class ActionService implements OnDestroy { this.seriesService.delete(series.id).subscribe((res: boolean) => { if (callback) { - this.toastr.success(translate('toasts.series-deleted')); + if (res) { + this.toastr.success(translate('toasts.series-deleted')); + } else { + this.toastr.error(translate('errors.generic')); + } + callback(res); } }); diff --git a/UI/Web/src/app/_services/library.service.ts b/UI/Web/src/app/_services/library.service.ts index 176ce9042..75abf3a03 100644 --- a/UI/Web/src/app/_services/library.service.ts +++ b/UI/Web/src/app/_services/library.service.ts @@ -1,11 +1,13 @@ import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import {DestroyRef, Injectable} from '@angular/core'; import { of } from 'rxjs'; -import { map } from 'rxjs/operators'; +import {filter, map, tap} from 'rxjs/operators'; import { environment } from 'src/environments/environment'; import { JumpKey } from '../_models/jumpbar/jump-key'; import { Library, LibraryType } from '../_models/library/library'; import { DirectoryDto } from '../_models/system/directory-dto'; +import {EVENTS, MessageHubService} from "./message-hub.service"; +import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; @Injectable({ @@ -18,7 +20,12 @@ export class LibraryService { private libraryNames: {[key:number]: string} | undefined = undefined; private libraryTypes: {[key: number]: LibraryType} | undefined = undefined; - constructor(private httpClient: HttpClient) {} + constructor(private httpClient: HttpClient, private readonly messageHub: MessageHubService, private readonly destroyRef: DestroyRef) { + this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef), filter(e => e.event === EVENTS.LibraryModified), + tap((e) => { + this.libraryNames = undefined; + })).subscribe(); + } getLibraryNames() { if (this.libraryNames != undefined) { diff --git a/UI/Web/src/app/_services/metadata.service.ts b/UI/Web/src/app/_services/metadata.service.ts index c3a21b8b5..dbd4b6a68 100644 --- a/UI/Web/src/app/_services/metadata.service.ts +++ b/UI/Web/src/app/_services/metadata.service.ts @@ -16,6 +16,8 @@ import {SortField} from "../_models/metadata/series-filter"; import {FilterCombination} from "../_models/metadata/v2/filter-combination"; import {SeriesFilterV2} from "../_models/metadata/v2/series-filter-v2"; import {FilterStatement} from "../_models/metadata/v2/filter-statement"; +import {SeriesDetailPlus} from "../_models/series-detail/series-detail-plus"; +import {LibraryType} from "../_models/library/library"; @Injectable({ providedIn: 'root' @@ -25,7 +27,15 @@ export class MetadataService { baseUrl = environment.apiUrl; private validLanguages: Array = []; - constructor(private httpClient: HttpClient, private router: Router) { } + constructor(private httpClient: HttpClient) { } + + getSeriesMetadataFromPlus(seriesId: number, libraryType: LibraryType) { + return this.httpClient.get(this.baseUrl + 'metadata/series-detail-plus?seriesId=' + seriesId + '&libraryType=' + libraryType); + } + + forceRefreshFromPlus(seriesId: number) { + return this.httpClient.post(this.baseUrl + 'metadata/force-refresh?seriesId=' + seriesId, {}); + } getAllAgeRatings(libraries?: Array) { let method = 'metadata/age-ratings' diff --git a/UI/Web/src/app/_services/nav.service.ts b/UI/Web/src/app/_services/nav.service.ts index 68df048f9..53eaac7fd 100644 --- a/UI/Web/src/app/_services/nav.service.ts +++ b/UI/Web/src/app/_services/nav.service.ts @@ -6,6 +6,8 @@ import {environment} from "../../environments/environment"; import {SideNavStream} from "../_models/sidenav/sidenav-stream"; import {TextResonse} from "../_types/text-response"; import {DashboardStream} from "../_models/dashboard/dashboard-stream"; +import {AccountService} from "./account.service"; +import {tap} from "rxjs/operators"; @Injectable({ providedIn: 'root' @@ -34,9 +36,16 @@ export class NavService { private renderer: Renderer2; baseUrl = environment.apiUrl; - constructor(@Inject(DOCUMENT) private document: Document, rendererFactory: RendererFactory2, private httpClient: HttpClient) { + constructor(@Inject(DOCUMENT) private document: Document, rendererFactory: RendererFactory2, private httpClient: HttpClient, private accountService: AccountService) { this.renderer = rendererFactory.createRenderer(null, null); - this.showNavBar(); + + // To avoid flashing, let's check if we are authenticated before we show + this.accountService.currentUser$.pipe(take(1)).subscribe(u => { + if (u) { + this.showNavBar(); + } + }); + const sideNavState = (localStorage.getItem(this.localStorageSideNavKey) === 'true') || false; this.sideNavCollapseSource.next(sideNavState); this.showSideNav(); diff --git a/UI/Web/src/app/_services/series.service.ts b/UI/Web/src/app/_services/series.service.ts index 84cda86a9..6de88b0aa 100644 --- a/UI/Web/src/app/_services/series.service.ts +++ b/UI/Web/src/app/_services/series.service.ts @@ -80,11 +80,11 @@ export class SeriesService { } delete(seriesId: number) { - return this.httpClient.delete(this.baseUrl + 'series/' + seriesId); + return this.httpClient.delete(this.baseUrl + 'series/' + seriesId, TextResonse).pipe(map(s => s === "true")); } deleteMultipleSeries(seriesIds: Array) { - return this.httpClient.post(this.baseUrl + 'series/delete-multiple', {seriesIds}); + return this.httpClient.post(this.baseUrl + 'series/delete-multiple', {seriesIds}, TextResonse).pipe(map(s => s === "true")); } updateRating(seriesId: number, userRating: number) { @@ -209,16 +209,21 @@ export class SeriesService { return this.httpClient.get(this.baseUrl + 'series/series-detail?seriesId=' + seriesId); } - getReviews(seriesId: number) { - return this.httpClient.get>(this.baseUrl + 'review?seriesId=' + seriesId); - } + + deleteReview(seriesId: number) { + return this.httpClient.delete(this.baseUrl + 'review?seriesId=' + seriesId); + } updateReview(seriesId: number, body: string) { return this.httpClient.post(this.baseUrl + 'review', { seriesId, body }); } + getReviews(seriesId: number) { + return this.httpClient.get>(this.baseUrl + 'review?seriesId=' + seriesId); + } + getRatings(seriesId: number) { return this.httpClient.get>(this.baseUrl + 'rating?seriesId=' + seriesId); } diff --git a/UI/Web/src/app/_services/server.service.ts b/UI/Web/src/app/_services/server.service.ts index 3f3a88a25..a70ef5a07 100644 --- a/UI/Web/src/app/_services/server.service.ts +++ b/UI/Web/src/app/_services/server.service.ts @@ -6,6 +6,7 @@ import { UpdateVersionEvent } from '../_models/events/update-version-event'; import { Job } from '../_models/job/job'; import { KavitaMediaError } from '../admin/_models/media-error'; import {TextResonse} from "../_types/text-response"; +import {map} from "rxjs/operators"; @Injectable({ providedIn: 'root' @@ -14,62 +15,63 @@ export class ServerService { baseUrl = environment.apiUrl; - constructor(private httpClient: HttpClient) { } + constructor(private http: HttpClient) { } getServerInfo() { - return this.httpClient.get(this.baseUrl + 'server/server-info-slim'); + return this.http.get(this.baseUrl + 'server/server-info-slim'); } clearCache() { - return this.httpClient.post(this.baseUrl + 'server/clear-cache', {}); + return this.http.post(this.baseUrl + 'server/clear-cache', {}); } cleanupWantToRead() { - return this.httpClient.post(this.baseUrl + 'server/cleanup-want-to-read', {}); + return this.http.post(this.baseUrl + 'server/cleanup-want-to-read', {}); } backupDatabase() { - return this.httpClient.post(this.baseUrl + 'server/backup-db', {}); + return this.http.post(this.baseUrl + 'server/backup-db', {}); } analyzeFiles() { - return this.httpClient.post(this.baseUrl + 'server/analyze-files', {}); + return this.http.post(this.baseUrl + 'server/analyze-files', {}); } checkForUpdate() { - return this.httpClient.get(this.baseUrl + 'server/check-update', {}); + return this.http.get(this.baseUrl + 'server/check-update'); + } + + checkHowOutOfDate() { + return this.http.get(this.baseUrl + 'server/checkHowOutOfDate', TextResonse) + .pipe(map(r => parseInt(r, 10))); + } + + checkForUpdates() { + return this.http.get(this.baseUrl + 'server/check-for-updates', {}); } getChangelog() { - return this.httpClient.get(this.baseUrl + 'server/changelog', {}); - } - - isServerAccessible() { - return this.httpClient.get(this.baseUrl + 'server/accessible'); + return this.http.get(this.baseUrl + 'server/changelog', {}); } getRecurringJobs() { - return this.httpClient.get(this.baseUrl + 'server/jobs'); + return this.http.get(this.baseUrl + 'server/jobs'); } convertMedia() { - return this.httpClient.post(this.baseUrl + 'server/convert-media', {}); + return this.http.post(this.baseUrl + 'server/convert-media', {}); } bustCache() { - return this.httpClient.post(this.baseUrl + 'server/bust-review-and-rec-cache', {}); + return this.http.post(this.baseUrl + 'server/bust-kavitaplus-cache', {}); } getMediaErrors() { - return this.httpClient.get>(this.baseUrl + 'server/media-errors', {}); + return this.http.get>(this.baseUrl + 'server/media-errors', {}); } clearMediaAlerts() { - return this.httpClient.post(this.baseUrl + 'server/clear-media-alerts', {}); - } - - getEmailVersion() { - return this.httpClient.get(this.baseUrl + 'server/email-version', TextResonse); + return this.http.post(this.baseUrl + 'server/clear-media-alerts', {}); } } diff --git a/UI/Web/src/app/_single-module/review-card/review-card.component.ts b/UI/Web/src/app/_single-module/review-card/review-card.component.ts index fe2b3b22f..7a0b29de2 100644 --- a/UI/Web/src/app/_single-module/review-card/review-card.component.ts +++ b/UI/Web/src/app/_single-module/review-card/review-card.component.ts @@ -1,10 +1,23 @@ -import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + inject, + Input, + OnInit, + Output +} from '@angular/core'; import {CommonModule, NgOptimizedImage} from '@angular/common'; import {UserReview} from "./user-review"; import {NgbModal} from "@ng-bootstrap/ng-bootstrap"; import {ReviewCardModalComponent} from "../review-card-modal/review-card-modal.component"; import {AccountService} from "../../_services/account.service"; -import {ReviewSeriesModalComponent} from "../review-series-modal/review-series-modal.component"; +import { + ReviewSeriesModalCloseAction, + ReviewSeriesModalCloseEvent, + ReviewSeriesModalComponent +} from "../review-series-modal/review-series-modal.component"; import {ReadMoreComponent} from "../../shared/read-more/read-more.component"; import {DefaultValuePipe} from "../../_pipes/default-value.pipe"; import {ImageComponent} from "../../shared/image/image.component"; @@ -25,6 +38,7 @@ export class ReviewCardComponent implements OnInit { protected readonly ScrobbleProvider = ScrobbleProvider; @Input({required: true}) review!: UserReview; + @Output() refresh = new EventEmitter(); isMyReview: boolean = false; @@ -48,5 +62,10 @@ export class ReviewCardComponent implements OnInit { } const ref = this.modalService.open(component, {size: 'lg', fullscreen: 'md'}); ref.componentInstance.review = this.review; + ref.closed.subscribe((res: ReviewSeriesModalCloseEvent | undefined) => { + if (res) { + this.refresh.emit(res); + } + }) } } diff --git a/UI/Web/src/app/_single-module/review-series-modal/review-series-modal.component.html b/UI/Web/src/app/_single-module/review-series-modal/review-series-modal.component.html index 9c4053135..90ff4867a 100644 --- a/UI/Web/src/app/_single-module/review-series-modal/review-series-modal.component.html +++ b/UI/Web/src/app/_single-module/review-series-modal/review-series-modal.component.html @@ -26,6 +26,7 @@ diff --git a/UI/Web/src/app/_single-module/review-series-modal/review-series-modal.component.ts b/UI/Web/src/app/_single-module/review-series-modal/review-series-modal.component.ts index 5aa539421..f7158b27c 100644 --- a/UI/Web/src/app/_single-module/review-series-modal/review-series-modal.component.ts +++ b/UI/Web/src/app/_single-module/review-series-modal/review-series-modal.component.ts @@ -1,10 +1,32 @@ -import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + inject, + Input, + OnInit +} from '@angular/core'; import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import {NgbActiveModal, NgbRating} from '@ng-bootstrap/ng-bootstrap'; import { SeriesService } from 'src/app/_services/series.service'; import {UserReview} from "../review-card/user-review"; import {CommonModule} from "@angular/common"; -import {TranslocoDirective} from "@ngneat/transloco"; +import {translate, TranslocoDirective} from "@ngneat/transloco"; +import {ConfirmService} from "../../shared/confirm.service"; +import {ToastrService} from "ngx-toastr"; + +export enum ReviewSeriesModalCloseAction { + Create, + Edit, + Delete, + Close +} +export interface ReviewSeriesModalCloseEvent { + success: boolean, + review: UserReview; + action: ReviewSeriesModalCloseAction +} @Component({ selector: 'app-review-series-modal', @@ -19,7 +41,9 @@ export class ReviewSeriesModalComponent implements OnInit { protected readonly modal = inject(NgbActiveModal); private readonly seriesService = inject(SeriesService); private readonly cdRef = inject(ChangeDetectorRef); - protected readonly minLength = 20; + private readonly confirmService = inject(ConfirmService); + private readonly toastr = inject(ToastrService); + protected readonly minLength = 5; @Input({required: true}) review!: UserReview; reviewGroup!: FormGroup; @@ -32,16 +56,23 @@ export class ReviewSeriesModalComponent implements OnInit { } close() { - this.modal.close({success: false, review: null}); + this.modal.close({success: false, review: this.review, action: ReviewSeriesModalCloseAction.Close}); } + async delete() { + if (!await this.confirmService.confirm(translate('toasts.delete-review'))) return; + this.seriesService.deleteReview(this.review.seriesId).subscribe(() => { + this.toastr.success(translate('toasts.review-deleted')); + this.modal.close({success: true, review: this.review, action: ReviewSeriesModalCloseAction.Delete}); + }); + } save() { const model = this.reviewGroup.value; if (model.reviewBody.length < this.minLength) { return; } - this.seriesService.updateReview(this.review.seriesId, model.reviewBody).subscribe(() => { - this.modal.close({success: true}); + this.seriesService.updateReview(this.review.seriesId, model.reviewBody).subscribe(review => { + this.modal.close({success: true, review: review, action: ReviewSeriesModalCloseAction.Edit}); }); } } diff --git a/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.html b/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.html index 9e5b01795..e1cf24d51 100644 --- a/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.html +++ b/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.html @@ -73,8 +73,13 @@ - - + @if(item.isProcessed) { + + } @else if (item.isErrored) { + + } @else { + + } {{item.isProcessed ? t('processed') : t('not-processed')}} diff --git a/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.scss b/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.scss index d1e48851e..7c4315507 100644 --- a/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.scss +++ b/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.scss @@ -1,3 +1,7 @@ .icon { color: var(--primary-color); } + +.error { + color: var(--error-color); +} diff --git a/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.ts b/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.ts index 0c52dabd9..ed1f76808 100644 --- a/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.ts +++ b/UI/Web/src/app/_single-module/user-scrobble-history/user-scrobble-history.component.ts @@ -1,34 +1,37 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core'; import {CommonModule} from '@angular/common'; -import {ScrobblingService} from "../../_services/scrobbling.service"; +import {ScrobbleProvider, ScrobblingService} from "../../_services/scrobbling.service"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {ScrobbleEvent, ScrobbleEventType} from "../../_models/scrobbling/scrobble-event"; import {ScrobbleEventTypePipe} from "../scrobble-event-type.pipe"; -import {NgbPagination} from "@ng-bootstrap/ng-bootstrap"; +import {NgbPagination, NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; import {ScrobbleEventSortField} from "../../_models/scrobbling/scrobble-event-filter"; import {debounceTime, take} from "rxjs/operators"; import {PaginatedResult, Pagination} from "../../_models/pagination"; import {SortableHeader, SortEvent} from "../table/_directives/sortable-header.directive"; import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms"; -import {TranslocoModule} from "@ngneat/transloco"; +import {translate, TranslocoModule} from "@ngneat/transloco"; import {DefaultValuePipe} from "../../_pipes/default-value.pipe"; import {TranslocoLocaleModule} from "@ngneat/transloco-locale"; import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe"; +import {ToastrService} from "ngx-toastr"; @Component({ selector: 'app-user-scrobble-history', standalone: true, - imports: [CommonModule, ScrobbleEventTypePipe, NgbPagination, ReactiveFormsModule, SortableHeader, TranslocoModule, DefaultValuePipe, TranslocoLocaleModule, UtcToLocalTimePipe], + imports: [CommonModule, ScrobbleEventTypePipe, NgbPagination, ReactiveFormsModule, SortableHeader, TranslocoModule, DefaultValuePipe, TranslocoLocaleModule, UtcToLocalTimePipe, NgbTooltip], templateUrl: './user-scrobble-history.component.html', styleUrls: ['./user-scrobble-history.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) export class UserScrobbleHistoryComponent implements OnInit { - private readonly scrobbleService = inject(ScrobblingService); + private readonly scrobblingService = inject(ScrobblingService); private readonly cdRef = inject(ChangeDetectorRef); private readonly destroyRef = inject(DestroyRef); + private readonly toastr = inject(ToastrService); + protected readonly ScrobbleEventType = ScrobbleEventType; pagination: Pagination | undefined; events: Array = []; @@ -36,11 +39,16 @@ export class UserScrobbleHistoryComponent implements OnInit { 'filter': new FormControl('', []) }); - get ScrobbleEventType() { return ScrobbleEventType; } - ngOnInit() { this.loadPage({column: 'createdUtc', direction: 'desc'}); + this.scrobblingService.hasTokenExpired(ScrobbleProvider.AniList).subscribe(hasExpired => { + if (hasExpired) { + this.toastr.error(translate('toasts.anilist-token-expired')); + } + this.cdRef.markForCheck(); + }); + this.formGroup.get('filter')?.valueChanges.pipe(debounceTime(200), takeUntilDestroyed(this.destroyRef)).subscribe(query => { this.loadPage(); }) @@ -73,7 +81,7 @@ export class UserScrobbleHistoryComponent implements OnInit { const field = this.mapSortColumnField(sortEvent?.column); const query = this.formGroup.get('filter')?.value; - this.scrobbleService.getScrobbleEvents({query, field, isDescending}, page, pageSize) + this.scrobblingService.getScrobbleEvents({query, field, isDescending}, page, pageSize) .pipe(take(1)) .subscribe((result: PaginatedResult) => { this.events = result.result; diff --git a/UI/Web/src/app/admin/_modals/directory-picker/directory-picker.component.ts b/UI/Web/src/app/admin/_modals/directory-picker/directory-picker.component.ts index 1584248db..2340bdba5 100644 --- a/UI/Web/src/app/admin/_modals/directory-picker/directory-picker.component.ts +++ b/UI/Web/src/app/admin/_modals/directory-picker/directory-picker.component.ts @@ -14,8 +14,6 @@ export interface DirectoryPickerResult { folderPath: string; } - - @Component({ selector: 'app-directory-picker', templateUrl: './directory-picker.component.html', @@ -129,13 +127,6 @@ export class DirectoryPickerComponent implements OnInit { }); } - shareFolder(fullPath: string, event: any) { - event.preventDefault(); - event.stopPropagation(); - - this.modal.close({success: true, folderPath: fullPath}); - } - share() { this.modal.close({success: true, folderPath: this.path}); } @@ -144,20 +135,6 @@ export class DirectoryPickerComponent implements OnInit { this.modal.close({success: false, folderPath: undefined}); } - getStem(path: string): string { - - const lastPath = this.routeStack.peek(); - if (lastPath && lastPath != path) { - let replaced = path.replace(lastPath, ''); - if (replaced.startsWith('/') || replaced.startsWith('\\')) { - replaced = replaced.substring(1, replaced.length); - } - return replaced; - } - - return path; - } - navigateTo(index: number) { while(this.routeStack.items.length - 1 > index) { this.routeStack.pop(); diff --git a/UI/Web/src/app/admin/_modals/reset-password-modal/reset-password-modal.component.ts b/UI/Web/src/app/admin/_modals/reset-password-modal/reset-password-modal.component.ts index b96619e4f..a764b3885 100644 --- a/UI/Web/src/app/admin/_modals/reset-password-modal/reset-password-modal.component.ts +++ b/UI/Web/src/app/admin/_modals/reset-password-modal/reset-password-modal.component.ts @@ -1,11 +1,12 @@ -import { Component, Input } from '@angular/core'; +import {Component, inject, Input} from '@angular/core'; import { FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angular/forms'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { Member } from 'src/app/_models/auth/member'; import { AccountService } from 'src/app/_services/account.service'; import { SentenceCasePipe } from '../../../_pipes/sentence-case.pipe'; import { NgIf } from '@angular/common'; -import {TranslocoDirective} from "@ngneat/transloco"; +import {translate, TranslocoDirective} from "@ngneat/transloco"; +import {ToastrService} from "ngx-toastr"; @Component({ selector: 'app-reset-password-modal', @@ -16,16 +17,21 @@ import {TranslocoDirective} from "@ngneat/transloco"; }) export class ResetPasswordModalComponent { + private readonly toastr = inject(ToastrService); + private readonly accountService = inject(AccountService); + public readonly modal = inject(NgbActiveModal); + @Input({required: true}) member!: Member; + errorMessage = ''; resetPasswordForm: FormGroup = new FormGroup({ password: new FormControl('', [Validators.required]), }); - constructor(public modal: NgbActiveModal, private accountService: AccountService) { } save() { this.accountService.resetPassword(this.member.username, this.resetPasswordForm.value.password,'').subscribe(() => { + this.toastr.success(translate('toasts.password-updated')) this.modal.close(); }); } diff --git a/UI/Web/src/app/admin/_models/server-settings.ts b/UI/Web/src/app/admin/_models/server-settings.ts index e58aa5190..47ef8ae07 100644 --- a/UI/Web/src/app/admin/_models/server-settings.ts +++ b/UI/Web/src/app/admin/_models/server-settings.ts @@ -1,10 +1,12 @@ import { EncodeFormat } from "./encode-format"; import {CoverImageSize} from "./cover-image-size"; +import {SmtpConfig} from "./smtp-config"; export interface ServerSettings { cacheDirectory: string; taskScan: string; taskBackup: string; + taskCleanup: string; loggingLevel: string; port: number; ipAddresses: string; @@ -22,4 +24,5 @@ export interface ServerSettings { onDeckProgressDays: number; onDeckUpdateDays: number; coverImageSize: CoverImageSize; + smtpConfig: SmtpConfig; } diff --git a/UI/Web/src/app/admin/_models/smtp-config.ts b/UI/Web/src/app/admin/_models/smtp-config.ts new file mode 100644 index 000000000..ee8c30f04 --- /dev/null +++ b/UI/Web/src/app/admin/_models/smtp-config.ts @@ -0,0 +1,11 @@ +export interface SmtpConfig { + senderAddress: string; + senderDisplayName: string; + userName: string; + password: string; + host: string; + port: number; + enableSsl: boolean; + sizeLimit: number; + customizedTemplates: boolean; +} diff --git a/UI/Web/src/app/admin/dashboard/dashboard.component.html b/UI/Web/src/app/admin/dashboard/dashboard.component.html index 0d52c86b0..16ded7f27 100644 --- a/UI/Web/src/app/admin/dashboard/dashboard.component.html +++ b/UI/Web/src/app/admin/dashboard/dashboard.component.html @@ -35,6 +35,7 @@

{{t('kavita+-desc-part-1')}} {{t('kavita+-desc-part-2')}} {{t('kavita+-desc-part-3')}} FAQ

+

{{t('kavita+-requirement')}} {{t('kavita+-releases')}}

diff --git a/UI/Web/src/app/admin/edit-user/edit-user.component.html b/UI/Web/src/app/admin/edit-user/edit-user.component.html index d4998ca8b..ab570b45a 100644 --- a/UI/Web/src/app/admin/edit-user/edit-user.component.html +++ b/UI/Web/src/app/admin/edit-user/edit-user.component.html @@ -28,9 +28,11 @@
- +
+ *ngIf="userForm.dirty || userForm.touched">
{{t('required')}}
diff --git a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html index 78a4007d0..5a98d7651 100644 --- a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html +++ b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.html @@ -1,33 +1,23 @@
-

{{t('title')}}

-

- {{t('send-to-warning')}} -

-
- - {{t('email-url-tooltip')}} - -
- - - -
-
+

{{t('title')}}

-
+

You must fill out both Host Name and SMTP settings to use email-based functionality within Kavita.

+ +
{{t('host-name-tooltip')}} - +
+ + + +
+
{{t('host-name-validation')}} @@ -35,7 +25,92 @@
+
+
+
+ + + {{t('sender-address-tooltip')}} + + +
+ + + +
+ + + {{t('sender-displayname-tooltip')}} + + +
+
+ +
+
+ + + {{t('host-tooltip')}} + + +
+ +
+ + +
+ +
+
+ + +
+
+
+ +
+
+ + + {{t('username-tooltip')}} + + +
+ +
+ + +
+
+ +
+
+ + + {{t('size-limit-tooltip')}} + + +
+ +
+
+ + + + + {{t('customized-templates-tooltip')}} + +
+
+
+
+ + +
+ diff --git a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts index eb17d2744..52d22d654 100644 --- a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts +++ b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts @@ -1,14 +1,20 @@ -import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core'; +import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core'; import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import {ToastrService} from 'ngx-toastr'; -import {forkJoin, take} from 'rxjs'; -import {EmailTestResult, SettingsService} from '../settings.service'; +import {take} from 'rxjs'; +import {SettingsService} from '../settings.service'; import {ServerSettings} from '../_models/server-settings'; -import {NgbTooltip} from '@ng-bootstrap/ng-bootstrap'; -import {NgIf, NgTemplateOutlet} from '@angular/common'; -import {translate, TranslocoModule, TranslocoService} from "@ngneat/transloco"; +import { + NgbAccordionBody, + NgbAccordionButton, + NgbAccordionCollapse, + NgbAccordionDirective, NgbAccordionHeader, NgbAccordionItem, + NgbTooltip +} from '@ng-bootstrap/ng-bootstrap'; +import {NgForOf, NgIf, NgTemplateOutlet, TitleCasePipe} from '@angular/common'; +import {translate, TranslocoModule} from "@ngneat/transloco"; import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe"; -import {ServerService} from "../../_services/server.service"; +import {ManageAlertsComponent} from "../manage-alerts/manage-alerts.component"; @Component({ selector: 'app-manage-email-settings', @@ -16,47 +22,84 @@ import {ServerService} from "../../_services/server.service"; styleUrls: ['./manage-email-settings.component.scss'], standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgIf, ReactiveFormsModule, NgbTooltip, NgTemplateOutlet, TranslocoModule, SafeHtmlPipe] + imports: [NgIf, ReactiveFormsModule, NgbTooltip, NgTemplateOutlet, TranslocoModule, SafeHtmlPipe, + ManageAlertsComponent, NgbAccordionBody, NgbAccordionButton, NgbAccordionCollapse, NgbAccordionDirective, + NgbAccordionHeader, NgbAccordionItem, NgForOf, TitleCasePipe] }) export class ManageEmailSettingsComponent implements OnInit { - serverSettings!: ServerSettings; - settingsForm: FormGroup = new FormGroup({}); - link = 'Kavita Email'; - emailVersion: string | null = null; private readonly cdRef = inject(ChangeDetectorRef); - private readonly serverService = inject(ServerService); private readonly settingsService = inject(SettingsService); private readonly toastr = inject(ToastrService); - constructor() { } + serverSettings!: ServerSettings; + settingsForm: FormGroup = new FormGroup({}); ngOnInit(): void { this.settingsService.getServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => { this.serverSettings = settings; - this.settingsForm.addControl('emailServiceUrl', new FormControl(this.serverSettings.emailServiceUrl, [Validators.required])); - this.settingsForm.addControl('hostName', new FormControl(this.serverSettings.hostName, [])); - this.cdRef.markForCheck(); - }); + this.settingsForm.addControl('hostName', new FormControl(this.serverSettings.hostName, [Validators.pattern(/^(http:|https:)+[^\s]+[\w]$/)])); + + this.settingsForm.addControl('host', new FormControl(this.serverSettings.smtpConfig.host, [])); + this.settingsForm.addControl('port', new FormControl(this.serverSettings.smtpConfig.port, [])); + this.settingsForm.addControl('userName', new FormControl(this.serverSettings.smtpConfig.userName, [])); + this.settingsForm.addControl('enableSsl', new FormControl(this.serverSettings.smtpConfig.enableSsl, [])); + this.settingsForm.addControl('password', new FormControl(this.serverSettings.smtpConfig.password, [])); + this.settingsForm.addControl('senderAddress', new FormControl(this.serverSettings.smtpConfig.senderAddress, [])); + this.settingsForm.addControl('senderDisplayName', new FormControl(this.serverSettings.smtpConfig.senderDisplayName, [])); + this.settingsForm.addControl('sizeLimit', new FormControl(this.serverSettings.smtpConfig.sizeLimit, [Validators.min(1)])); + this.settingsForm.addControl('customizedTemplates', new FormControl(this.serverSettings.smtpConfig.customizedTemplates, [Validators.min(1)])); - this.serverService.getEmailVersion().subscribe(version => { - this.emailVersion = version; this.cdRef.markForCheck(); }); } resetForm() { - this.settingsForm.get('emailServiceUrl')?.setValue(this.serverSettings.emailServiceUrl); this.settingsForm.get('hostName')?.setValue(this.serverSettings.hostName); + + this.settingsForm.addControl('host', new FormControl(this.serverSettings.smtpConfig.host, [])); + this.settingsForm.addControl('port', new FormControl(this.serverSettings.smtpConfig.port, [])); + this.settingsForm.addControl('userName', new FormControl(this.serverSettings.smtpConfig.userName, [])); + this.settingsForm.addControl('enableSsl', new FormControl(this.serverSettings.smtpConfig.enableSsl, [])); + this.settingsForm.addControl('password', new FormControl(this.serverSettings.smtpConfig.password, [])); + this.settingsForm.addControl('senderAddress', new FormControl(this.serverSettings.smtpConfig.senderAddress, [])); + this.settingsForm.addControl('senderDisplayName', new FormControl(this.serverSettings.smtpConfig.senderDisplayName, [])); + this.settingsForm.addControl('sizeLimit', new FormControl(this.serverSettings.smtpConfig.sizeLimit, [Validators.min(1)])); + this.settingsForm.addControl('customizedTemplates', new FormControl(this.serverSettings.smtpConfig.customizedTemplates, [Validators.min(1)])); this.settingsForm.markAsPristine(); this.cdRef.markForCheck(); } + autofillGmail() { + this.settingsForm.get('host')?.setValue('smtp.gmail.com'); + this.settingsForm.get('port')?.setValue(587); + this.settingsForm.get('sizeLimit')?.setValue(26214400); + this.settingsForm.get('enableSsl')?.setValue(true); + this.cdRef.markForCheck(); + } + + autofillOutlook() { + this.settingsForm.get('host')?.setValue('smtp-mail.outlook.com'); + this.settingsForm.get('port')?.setValue(587 ); + this.settingsForm.get('sizeLimit')?.setValue(1048576); + this.settingsForm.get('enableSsl')?.setValue(true); + this.cdRef.markForCheck(); + } + async saveSettings() { const modelSettings = Object.assign({}, this.serverSettings); modelSettings.emailServiceUrl = this.settingsForm.get('emailServiceUrl')?.value; modelSettings.hostName = this.settingsForm.get('hostName')?.value; + modelSettings.smtpConfig.host = this.settingsForm.get('host')?.value; + modelSettings.smtpConfig.port = this.settingsForm.get('port')?.value; + modelSettings.smtpConfig.userName = this.settingsForm.get('userName')?.value; + modelSettings.smtpConfig.enableSsl = this.settingsForm.get('enableSsl')?.value; + modelSettings.smtpConfig.password = this.settingsForm.get('password')?.value; + modelSettings.smtpConfig.senderAddress = this.settingsForm.get('senderAddress')?.value; + modelSettings.smtpConfig.senderDisplayName = this.settingsForm.get('senderDisplayName')?.value; + modelSettings.smtpConfig.sizeLimit = this.settingsForm.get('sizeLimit')?.value; + modelSettings.smtpConfig.customizedTemplates = this.settingsForm.get('customizedTemplates')?.value; this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe((settings: ServerSettings) => { this.serverSettings = settings; @@ -77,32 +120,13 @@ export class ManageEmailSettingsComponent implements OnInit { }); } - resetEmailServiceUrl() { - this.settingsService.resetEmailServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => { - this.serverSettings.emailServiceUrl = settings.emailServiceUrl; - this.resetForm(); - this.toastr.success(translate('toasts.email-service-reset')); - }, (err: any) => { - console.error('error: ', err); - }); - } - - testEmailServiceUrl() { - if (this.settingsForm.get('emailServiceUrl')?.value === '') return; - forkJoin([this.settingsService.testEmailServerSettings(this.settingsForm.get('emailServiceUrl')?.value), this.serverService.getEmailVersion()]) - .pipe(take(1)).subscribe(async (results) => { - const result = results[0] as EmailTestResult; - if (result.successful) { - const version = ('. Kavita Email: ' + results[1] ? 'v' + results[1] : ''); - this.toastr.success(translate('toasts.email-service-reachable') + ' - ' + version); + test() { + this.settingsService.testEmailServerSettings().subscribe(res => { + if (res.successful) { + this.toastr.success(translate('toasts.email-sent', {email: res.emailAddress})); } else { - this.toastr.error(translate('toasts.email-service-unresponsive') + result.errorMessage.split('(')[0]); + this.toastr.error(translate('toasts.email-not-sent-test')) } - - }, (err: any) => { - console.error('error: ', err); }); - } - } diff --git a/UI/Web/src/app/admin/manage-library/manage-library.component.ts b/UI/Web/src/app/admin/manage-library/manage-library.component.ts index bf48db286..692ab4558 100644 --- a/UI/Web/src/app/admin/manage-library/manage-library.component.ts +++ b/UI/Web/src/app/admin/manage-library/manage-library.component.ts @@ -24,6 +24,7 @@ import { RouterLink } from '@angular/router'; import { NgFor, NgIf } from '@angular/common'; import {translate, TranslocoModule} from "@ngneat/transloco"; import {DefaultDatePipe} from "../../_pipes/default-date.pipe"; +import {ActionService} from "../../_services/action.service"; @Component({ selector: 'app-manage-library', @@ -35,6 +36,15 @@ import {DefaultDatePipe} from "../../_pipes/default-date.pipe"; }) export class ManageLibraryComponent implements OnInit { + private readonly actionService = inject(ActionService); + private readonly libraryService = inject(LibraryService); + private readonly modalService = inject(NgbModal); + private readonly toastr = inject(ToastrService); + private readonly confirmService = inject(ConfirmService); + private readonly hubService = inject(MessageHubService); + private readonly cdRef = inject(ChangeDetectorRef); + private readonly destroyRef = inject(DestroyRef); + libraries: Library[] = []; loading = false; /** @@ -42,11 +52,8 @@ export class ManageLibraryComponent implements OnInit { */ deletionInProgress: boolean = false; libraryTrackBy = (index: number, item: Library) => `${item.name}_${item.lastScanned}_${item.type}_${item.folders.length}`; - private readonly destroyRef = inject(DestroyRef); - constructor(private modalService: NgbModal, private libraryService: LibraryService, - private toastr: ToastrService, private confirmService: ConfirmService, - private hubService: MessageHubService, private readonly cdRef: ChangeDetectorRef) { } + ngOnInit(): void { this.getLibraries(); diff --git a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html index 58d89cb7d..06af00124 100644 --- a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html +++ b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html @@ -5,6 +5,21 @@ {{t('notice')}} {{t('restart-required')}}
+
+ + {{t('host-name-tooltip')}} + + + + +
+
+ {{t('host-name-validation')}} +
+
+
+
{{t('base-url-tooltip')}} diff --git a/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts b/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts index 5adbf600c..118766e3a 100644 --- a/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts +++ b/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts @@ -45,6 +45,7 @@ export class ManageSettingsComponent implements OnInit { this.settingsForm.addControl('cacheDirectory', new FormControl(this.serverSettings.cacheDirectory, [Validators.required])); this.settingsForm.addControl('taskScan', new FormControl(this.serverSettings.taskScan, [Validators.required])); this.settingsForm.addControl('taskBackup', new FormControl(this.serverSettings.taskBackup, [Validators.required])); + this.settingsForm.addControl('taskCleanup', new FormControl(this.serverSettings.taskCleanup, [Validators.required])); this.settingsForm.addControl('ipAddresses', new FormControl(this.serverSettings.ipAddresses, [Validators.required, Validators.pattern(ValidIpAddress)])); this.settingsForm.addControl('port', new FormControl(this.serverSettings.port, [Validators.required])); this.settingsForm.addControl('loggingLevel', new FormControl(this.serverSettings.loggingLevel, [Validators.required])); @@ -77,6 +78,7 @@ export class ManageSettingsComponent implements OnInit { this.settingsForm.get('cacheDirectory')?.setValue(this.serverSettings.cacheDirectory); this.settingsForm.get('scanTask')?.setValue(this.serverSettings.taskScan); this.settingsForm.get('taskBackup')?.setValue(this.serverSettings.taskBackup); + this.settingsForm.get('taskCleanup')?.setValue(this.serverSettings.taskCleanup); this.settingsForm.get('ipAddresses')?.setValue(this.serverSettings.ipAddresses); this.settingsForm.get('port')?.setValue(this.serverSettings.port); this.settingsForm.get('loggingLevel')?.setValue(this.serverSettings.loggingLevel); @@ -99,6 +101,9 @@ export class ManageSettingsComponent implements OnInit { async saveSettings() { const modelSettings = this.settingsForm.value; modelSettings.bookmarksDirectory = this.serverSettings.bookmarksDirectory; + modelSettings.smtpConfig = this.serverSettings.smtpConfig; + + this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe((settings: ServerSettings) => { this.serverSettings = settings; this.resetForm(); diff --git a/UI/Web/src/app/admin/manage-system/manage-system.component.html b/UI/Web/src/app/admin/manage-system/manage-system.component.html index 954dc4eab..34583edb7 100644 --- a/UI/Web/src/app/admin/manage-system/manage-system.component.html +++ b/UI/Web/src/app/admin/manage-system/manage-system.component.html @@ -1,8 +1,8 @@
-

{{t('title')}}

-
+
+

{{t('title')}}

{{t('version-title')}}
{{serverInfo.kavitaVersion}}
@@ -12,32 +12,43 @@
-

{{t('more-info-title')}}

-
-
-
{{t('home-page-title')}}
- +
+

{{t('more-info-title')}}

+
+
{{t('home-page-title')}}
+ +
+
+
{{t('wiki-title')}}
+ +
+
+
{{t('discord-title')}}
+ +
+
+
{{t('donations-title')}}
+ +
+
+
{{t('source-title')}}
+ +
+
+
{{t('localization-title')}}
+ +
+
+
{{t('feature-request-title')}}
+ +
-
-
{{t('wiki-title')}}
- -
-
-
{{t('discord-title')}}
- -
-
-
{{t('donations-title')}}
- -
-
-
{{t('source-title')}}
- -
-
-
{{t('feature-request-title')}}
- + +
+

{{t('updates-title')}}

+
+
diff --git a/UI/Web/src/app/admin/manage-system/manage-system.component.ts b/UI/Web/src/app/admin/manage-system/manage-system.component.ts index 4e6467ce9..5d8976e3b 100644 --- a/UI/Web/src/app/admin/manage-system/manage-system.component.ts +++ b/UI/Web/src/app/admin/manage-system/manage-system.component.ts @@ -3,6 +3,7 @@ import {ServerService} from 'src/app/_services/server.service'; import {ServerInfoSlim} from '../_models/server-info'; import {NgIf} from '@angular/common'; import {TranslocoDirective} from "@ngneat/transloco"; +import {ChangelogComponent} from "../../announcements/_components/changelog/changelog.component"; @Component({ selector: 'app-manage-system', @@ -10,7 +11,7 @@ import {TranslocoDirective} from "@ngneat/transloco"; styleUrls: ['./manage-system.component.scss'], standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgIf, TranslocoDirective] + imports: [NgIf, TranslocoDirective, ChangelogComponent] }) export class ManageSystemComponent implements OnInit { diff --git a/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.html b/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.html index 5816a1c28..2ff0c3efe 100644 --- a/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.html +++ b/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.html @@ -7,8 +7,30 @@ {{t('library-scan-tooltip')}} + + @if (settingsForm.get('taskScan')!.value === customOption) { +
+ + + + @if (settingsForm.dirty || settingsForm.touched) { +
+
+ {{t('required')}} +
+
+ {{t('cron-notation')}} +
+
+ } +
+ } +
@@ -16,8 +38,65 @@ {{t('library-database-backup-tooltip')}} + + @if (settingsForm.get('taskBackup')!.value === customOption) { +
+ + + + @if (settingsForm.dirty || settingsForm.touched) { +
+
+ {{t('required')}} +
+
+ {{t('cron-notation')}} +
+
+ } +
+ } +
+ +
+ + {{t('cleanup-tooltip')}} + + + + @if (settingsForm.get('taskCleanup')!.value === customOption) { +
+ + + + @if (settingsForm.dirty || settingsForm.touched) { +
+
+ {{t('required')}} +
+
+ {{t('cron-notation')}} +
+
+ } +
+ } +
+ +
+ + +

{{t('adhoc-tasks-title')}}

@@ -65,13 +144,6 @@ - - -
- - - -
diff --git a/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.ts b/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.ts index eb5552409..68d07fd21 100644 --- a/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.ts +++ b/UI/Web/src/app/admin/manage-tasks-settings/manage-tasks-settings.component.ts @@ -1,10 +1,10 @@ -import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core'; +import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core'; import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import {ToastrService} from 'ngx-toastr'; import {SettingsService} from '../settings.service'; import {ServerSettings} from '../_models/server-settings'; import {shareReplay, take} from 'rxjs/operators'; -import {defer, forkJoin, Observable, of} from 'rxjs'; +import {debounceTime, defer, distinctUntilChanged, forkJoin, Observable, of, switchMap, tap} from 'rxjs'; import {ServerService} from 'src/app/_services/server.service'; import {Job} from 'src/app/_models/job/job'; import {UpdateNotificationModalComponent} from 'src/app/shared/update-notification/update-notification-modal.component'; @@ -12,10 +12,12 @@ import {NgbModal, NgbTooltip} from '@ng-bootstrap/ng-bootstrap'; import {DownloadService} from 'src/app/shared/_services/download.service'; import {DefaultValuePipe} from '../../_pipes/default-value.pipe'; import {AsyncPipe, DatePipe, NgFor, NgIf, NgTemplateOutlet, TitleCasePipe} from '@angular/common'; -import {TranslocoModule, TranslocoService} from "@ngneat/transloco"; +import {translate, TranslocoModule} from "@ngneat/transloco"; import {TranslocoLocaleModule} from "@ngneat/transloco-locale"; import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe"; +import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; + interface AdhocTask { name: string; description: string; @@ -30,15 +32,18 @@ interface AdhocTask { styleUrls: ['./manage-tasks-settings.component.scss'], standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgIf, ReactiveFormsModule, NgbTooltip, NgFor, AsyncPipe, TitleCasePipe, DatePipe, DefaultValuePipe, TranslocoModule, NgTemplateOutlet, TranslocoLocaleModule, UtcToLocalTimePipe] + imports: [NgIf, ReactiveFormsModule, NgbTooltip, NgFor, AsyncPipe, TitleCasePipe, DatePipe, DefaultValuePipe, + TranslocoModule, NgTemplateOutlet, TranslocoLocaleModule, UtcToLocalTimePipe] }) export class ManageTasksSettingsComponent implements OnInit { - private readonly translocoService = inject(TranslocoService); private readonly cdRef = inject(ChangeDetectorRef); + private readonly destroyRef = inject(DestroyRef); + serverSettings!: ServerSettings; settingsForm: FormGroup = new FormGroup({}); taskFrequencies: Array = []; + taskFrequenciesForCleanup: Array = []; logLevels: Array = []; recurringTasks$: Observable> = of([]); @@ -50,12 +55,13 @@ export class ManageTasksSettingsComponent implements OnInit { api: this.serverService.convertMedia(), successMessage: 'convert-media-task-success' }, - { - name: 'bust-cache-task', - description: 'bust-cache-task-desc', - api: this.serverService.bustCache(), - successMessage: 'bust-cache-task-success' - }, + // I removed this as it's not really needed, given that External Recs are the only thing that fill this cache now + // { + // name: 'bust-cache-task', + // description: 'bust-cache-task-desc', + // api: this.serverService.bustCache(), + // successMessage: 'bust-cache-task-success' + // }, { name: 'bust-locale-task', description: 'bust-locale-task-desc', @@ -104,7 +110,7 @@ export class ManageTasksSettingsComponent implements OnInit { successMessage: '', successFunction: (update) => { if (update === null) { - this.toastr.info(this.translocoService.translate('toasts.no-updates')); + this.toastr.info(translate('toasts.no-updates')); return; } const modalRef = this.modalService.open(UpdateNotificationModalComponent, { scrollable: true, size: 'lg' }); @@ -112,6 +118,7 @@ export class ManageTasksSettingsComponent implements OnInit { } }, ]; + customOption = 'custom'; constructor(private settingsService: SettingsService, private toastr: ToastrService, private serverService: ServerService, private modalService: NgbModal, @@ -124,10 +131,79 @@ export class ManageTasksSettingsComponent implements OnInit { settings: this.settingsService.getServerSettings() }).subscribe(result => { this.taskFrequencies = result.frequencies; + this.taskFrequencies.push(this.customOption); + + this.taskFrequenciesForCleanup = this.taskFrequencies.filter(f => f !== 'disabled'); + this.logLevels = result.levels; this.serverSettings = result.settings; this.settingsForm.addControl('taskScan', new FormControl(this.serverSettings.taskScan, [Validators.required])); this.settingsForm.addControl('taskBackup', new FormControl(this.serverSettings.taskBackup, [Validators.required])); + this.settingsForm.addControl('taskCleanup', new FormControl(this.serverSettings.taskCleanup, [Validators.required])); + + if (!this.taskFrequencies.includes(this.serverSettings.taskScan)) { + this.settingsForm.get('taskScan')?.setValue(this.customOption); + this.settingsForm.addControl('taskScanCustom', new FormControl(this.serverSettings.taskScan, [Validators.required])); + } else { + this.settingsForm.addControl('taskScanCustom', new FormControl('', [Validators.required])); + } + + if (!this.taskFrequencies.includes(this.serverSettings.taskBackup)) { + this.settingsForm.get('taskBackup')?.setValue(this.customOption); + this.settingsForm.addControl('taskBackupCustom', new FormControl(this.serverSettings.taskBackup, [Validators.required])); + } else { + this.settingsForm.addControl('taskBackupCustom', new FormControl('', [Validators.required])); + } + + if (!this.taskFrequenciesForCleanup.includes(this.serverSettings.taskCleanup)) { + this.settingsForm.get('taskCleanup')?.setValue(this.customOption); + this.settingsForm.addControl('taskCleanupCustom', new FormControl(this.serverSettings.taskCleanup, [Validators.required])); + } else { + this.settingsForm.addControl('taskCleanupCustom', new FormControl('', [Validators.required])); + } + + this.settingsForm.get('taskScanCustom')?.valueChanges.pipe( + debounceTime(100), + switchMap(val => this.settingsService.isValidCronExpression(val)), + tap(isValid => { + if (isValid) { + this.settingsForm.get('taskScanCustom')?.setErrors(null); + } else { + this.settingsForm.get('taskScanCustom')?.setErrors({invalidCron: true}) + } + this.cdRef.markForCheck(); + }), + takeUntilDestroyed(this.destroyRef) + ).subscribe(); + + this.settingsForm.get('taskBackupCustom')?.valueChanges.pipe( + debounceTime(100), + switchMap(val => this.settingsService.isValidCronExpression(val)), + tap(isValid => { + if (isValid) { + this.settingsForm.get('taskBackupCustom')?.setErrors(null); + } else { + this.settingsForm.get('taskBackupCustom')?.setErrors({invalidCron: true}) + } + this.cdRef.markForCheck(); + }), + takeUntilDestroyed(this.destroyRef) + ).subscribe(); + + this.settingsForm.get('taskCleanupCustom')?.valueChanges.pipe( + debounceTime(100), + switchMap(val => this.settingsService.isValidCronExpression(val)), + tap(isValid => { + if (isValid) { + this.settingsForm.get('taskCleanupCustom')?.setErrors(null); + } else { + this.settingsForm.get('taskCleanupCustom')?.setErrors({invalidCron: true}) + } + this.cdRef.markForCheck(); + }), + takeUntilDestroyed(this.destroyRef) + ).subscribe(); + this.cdRef.markForCheck(); }); @@ -135,9 +211,30 @@ export class ManageTasksSettingsComponent implements OnInit { this.cdRef.markForCheck(); } + resetForm() { this.settingsForm.get('taskScan')?.setValue(this.serverSettings.taskScan); this.settingsForm.get('taskBackup')?.setValue(this.serverSettings.taskBackup); + this.settingsForm.get('taskCleanup')?.setValue(this.serverSettings.taskCleanup); + + if (!this.taskFrequencies.includes(this.serverSettings.taskScan)) { + this.settingsForm.get('taskScanCustom')?.setValue(this.serverSettings.taskScan); + } else { + this.settingsForm.get('taskScanCustom')?.setValue(''); + } + + if (!this.taskFrequencies.includes(this.serverSettings.taskBackup)) { + this.settingsForm.get('taskBackupCustom')?.setValue(this.serverSettings.taskBackup); + } else { + this.settingsForm.get('taskBackupCustom')?.setValue(''); + } + + if (!this.taskFrequencies.includes(this.serverSettings.taskCleanup)) { + this.settingsForm.get('taskCleanupCustom')?.setValue(this.serverSettings.taskCleanup); + } else { + this.settingsForm.get('taskCleanupCustom')?.setValue(''); + } + this.settingsForm.markAsPristine(); this.cdRef.markForCheck(); } @@ -146,12 +243,26 @@ export class ManageTasksSettingsComponent implements OnInit { const modelSettings = Object.assign({}, this.serverSettings); modelSettings.taskBackup = this.settingsForm.get('taskBackup')?.value; modelSettings.taskScan = this.settingsForm.get('taskScan')?.value; + modelSettings.taskCleanup = this.settingsForm.get('taskCleanup')?.value; + + if (this.serverSettings.taskBackup === this.customOption) { + modelSettings.taskBackup = this.settingsForm.get('taskBackupCustom')?.value; + } + + if (this.serverSettings.taskScan === this.customOption) { + modelSettings.taskScan = this.settingsForm.get('taskScanCustom')?.value; + } + + if (this.serverSettings.taskScan === this.customOption) { + modelSettings.taskCleanup = this.settingsForm.get('taskCleanupCustom')?.value; + } + this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe(async (settings: ServerSettings) => { this.serverSettings = settings; this.resetForm(); this.recurringTasks$ = this.serverService.getRecurringJobs().pipe(shareReplay()); - this.toastr.success(this.translocoService.translate('toasts.server-settings-updated')); + this.toastr.success(translate('toasts.server-settings-updated')); this.cdRef.markForCheck(); }, (err: any) => { console.error('error: ', err); @@ -162,7 +273,7 @@ export class ManageTasksSettingsComponent implements OnInit { this.settingsService.resetServerSettings().pipe(take(1)).subscribe(async (settings: ServerSettings) => { this.serverSettings = settings; this.resetForm(); - this.toastr.success(this.translocoService.translate('toasts.server-settings-updated')); + this.toastr.success(translate('toasts.server-settings-updated')); }, (err: any) => { console.error('error: ', err); }); @@ -171,7 +282,7 @@ export class ManageTasksSettingsComponent implements OnInit { runAdhoc(task: AdhocTask) { task.api.subscribe((data: any) => { if (task.successMessage.length > 0) { - this.toastr.success(this.translocoService.translate('manage-tasks-settings.' + task.successMessage)); + this.toastr.success(translate('manage-tasks-settings.' + task.successMessage)); } if (task.successFunction) { diff --git a/UI/Web/src/app/admin/manage-users/manage-users.component.html b/UI/Web/src/app/admin/manage-users/manage-users.component.html index 9232a069e..5ea7d766d 100644 --- a/UI/Web/src/app/admin/manage-users/manage-users.component.html +++ b/UI/Web/src/app/admin/manage-users/manage-users.component.html @@ -48,7 +48,7 @@ {{t('roles-title')}} {{null | defaultValue}} - Admin + {{t('admin')}} {{role}} diff --git a/UI/Web/src/app/admin/manage-users/manage-users.component.ts b/UI/Web/src/app/admin/manage-users/manage-users.component.ts index 1394a2eca..bdea28f41 100644 --- a/UI/Web/src/app/admin/manage-users/manage-users.component.ts +++ b/UI/Web/src/app/admin/manage-users/manage-users.component.ts @@ -99,7 +99,7 @@ export class ManageUsersComponent implements OnInit { setTimeout(() => { this.loadMembers(); this.toastr.success(this.translocoService.translate('toasts.user-deleted', {user: member.username})); - }, 30); // SetTimeout because I've noticed this can run superfast and not give enough time for data to flush + }, 30); // SetTimeout because I've noticed this can run super fast and not give enough time for data to flush }); } } @@ -112,15 +112,13 @@ export class ManageUsersComponent implements OnInit { } resendEmail(member: Member) { - this.serverService.isServerAccessible().subscribe(canAccess => { - this.accountService.resendConfirmationEmail(member.id).subscribe(async (email) => { - if (canAccess) { - this.toastr.info(this.translocoService.translate('toasts.email-sent', {user: member.username})); - return; - } - await this.confirmService.alert( - this.translocoService.translate('toasts.click-email-link') + '
' + email + ''); - }); + this.accountService.resendConfirmationEmail(member.id).subscribe(async (response) => { + if (response.emailSent) { + this.toastr.info(this.translocoService.translate('toasts.email-sent', {email: member.username})); + return; + } + await this.confirmService.alert( + this.translocoService.translate('toasts.click-email-link') + '
' + response.emailLink + ''); }); } diff --git a/UI/Web/src/app/admin/settings.service.ts b/UI/Web/src/app/admin/settings.service.ts index 558eade38..4b0d30b8d 100644 --- a/UI/Web/src/app/admin/settings.service.ts +++ b/UI/Web/src/app/admin/settings.service.ts @@ -1,6 +1,6 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { map } from 'rxjs'; +import {map, of} from 'rxjs'; import { environment } from 'src/environments/environment'; import { TextResonse } from '../_types/text-response'; import { ServerSettings } from './_models/server-settings'; @@ -11,6 +11,7 @@ import { ServerSettings } from './_models/server-settings'; export interface EmailTestResult { successful: boolean; errorMessage: string; + emailAddress: string; } @Injectable({ @@ -26,10 +27,6 @@ export class SettingsService { return this.http.get(this.baseUrl + 'settings'); } - getBaseUrl() { - return this.http.get(this.baseUrl + 'settings/base-url', TextResonse); - } - updateServerSettings(model: ServerSettings) { return this.http.post(this.baseUrl + 'settings', model); } @@ -46,12 +43,12 @@ export class SettingsService { return this.http.post(this.baseUrl + 'settings/reset-base-url', {}); } - resetEmailServerSettings() { - return this.http.post(this.baseUrl + 'settings/reset-email-url', {}); + testEmailServerSettings() { + return this.http.post(this.baseUrl + 'settings/test-email-url', {}); } - testEmailServerSettings(emailUrl: string) { - return this.http.post(this.baseUrl + 'settings/test-email-url', {url: emailUrl}); + isEmailSetup() { + return this.http.get(this.baseUrl + 'server/is-email-setup', TextResonse).pipe(map(d => d == "true")); } getTaskFrequencies() { @@ -69,4 +66,10 @@ export class SettingsService { getOpdsEnabled() { return this.http.get(this.baseUrl + 'settings/opds-enabled', TextResonse).pipe(map(d => d === 'true')); } + + isValidCronExpression(val: string) { + if (val === '' || val === undefined || val === null) return of(false); + return this.http.get(this.baseUrl + 'settings/is-valid-cron?cronExpression=' + val, TextResonse).pipe(map(d => d === 'true')); + + } } diff --git a/UI/Web/src/app/announcements/_components/changelog/changelog.component.html b/UI/Web/src/app/announcements/_components/changelog/changelog.component.html index c63e81046..68e82e28c 100644 --- a/UI/Web/src/app/announcements/_components/changelog/changelog.component.html +++ b/UI/Web/src/app/announcements/_components/changelog/changelog.component.html @@ -9,8 +9,13 @@

{{update.updateTitle}}  - {{t('installed')}} - {{t('available')}} + @if (update.isOnNightlyInRelease) { + {{t('nightly', {version: update.currentVersion})}} + } @else if (update.isReleaseEqual) { + {{t('installed')}} + } @else if (update.isReleaseNewer && indx === 0) { + {{t('available')}} + }

{{t('published-label')}}{{update.publishDate | date: 'short'}}
diff --git a/UI/Web/src/app/announcements/_components/changelog/changelog.component.ts b/UI/Web/src/app/announcements/_components/changelog/changelog.component.ts index f2f1fdcd1..65e7be632 100644 --- a/UI/Web/src/app/announcements/_components/changelog/changelog.component.ts +++ b/UI/Web/src/app/announcements/_components/changelog/changelog.component.ts @@ -1,30 +1,31 @@ -import { Component, OnInit } from '@angular/core'; -import { UpdateVersionEvent } from 'src/app/_models/events/update-version-event'; -import { ServerService } from 'src/app/_services/server.service'; -import { LoadingComponent } from '../../../shared/loading/loading.component'; -import { ReadMoreComponent } from '../../../shared/read-more/read-more.component'; -import { NgFor, NgIf, DatePipe } from '@angular/common'; +import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core'; +import {UpdateVersionEvent} from 'src/app/_models/events/update-version-event'; +import {ServerService} from 'src/app/_services/server.service'; +import {LoadingComponent} from '../../../shared/loading/loading.component'; +import {ReadMoreComponent} from '../../../shared/read-more/read-more.component'; +import {DatePipe, NgFor, NgIf} from '@angular/common'; import {TranslocoDirective} from "@ngneat/transloco"; @Component({ - selector: 'app-changelog', - templateUrl: './changelog.component.html', - styleUrls: ['./changelog.component.scss'], - standalone: true, - imports: [NgFor, NgIf, ReadMoreComponent, LoadingComponent, DatePipe, TranslocoDirective] + selector: 'app-changelog', + templateUrl: './changelog.component.html', + styleUrls: ['./changelog.component.scss'], + standalone: true, + imports: [NgFor, NgIf, ReadMoreComponent, LoadingComponent, DatePipe, TranslocoDirective], + changeDetection: ChangeDetectionStrategy.OnPush }) export class ChangelogComponent implements OnInit { + private readonly serverService = inject(ServerService); + private readonly cdRef = inject(ChangeDetectorRef); updates: Array = []; isLoading: boolean = true; - constructor(private serverService: ServerService) { } - ngOnInit(): void { - this.serverService.getChangelog().subscribe(updates => { this.updates = updates; this.isLoading = false; + this.cdRef.markForCheck(); }); } } diff --git a/UI/Web/src/app/announcements/_components/out-of-date-modal/out-of-date-modal.component.html b/UI/Web/src/app/announcements/_components/out-of-date-modal/out-of-date-modal.component.html new file mode 100644 index 000000000..ddbba7338 --- /dev/null +++ b/UI/Web/src/app/announcements/_components/out-of-date-modal/out-of-date-modal.component.html @@ -0,0 +1,18 @@ + + + + + + diff --git a/UI/Web/src/app/announcements/_components/out-of-date-modal/out-of-date-modal.component.scss b/UI/Web/src/app/announcements/_components/out-of-date-modal/out-of-date-modal.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/UI/Web/src/app/announcements/_components/out-of-date-modal/out-of-date-modal.component.ts b/UI/Web/src/app/announcements/_components/out-of-date-modal/out-of-date-modal.component.ts new file mode 100644 index 000000000..41e6f36d9 --- /dev/null +++ b/UI/Web/src/app/announcements/_components/out-of-date-modal/out-of-date-modal.component.ts @@ -0,0 +1,38 @@ +import {Component, DestroyRef, inject, Input} from '@angular/core'; +import {FormsModule} from "@angular/forms"; +import {AsyncPipe, NgForOf, NgIf} from "@angular/common"; +import {NgbActiveModal, NgbHighlight, NgbModal, NgbTypeahead} from "@ng-bootstrap/ng-bootstrap"; +import {TranslocoDirective} from "@ngneat/transloco"; +import {ServerService} from "../../../_services/server.service"; +import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; +import {map} from "rxjs/operators"; +import {ChangelogComponent} from "../changelog/changelog.component"; +import {SafeHtmlPipe} from "../../../_pipes/safe-html.pipe"; + +@Component({ + selector: 'app-out-of-date-modal', + standalone: true, + imports: [ + FormsModule, + NgForOf, + NgIf, + NgbHighlight, + NgbTypeahead, + TranslocoDirective, + AsyncPipe, + ChangelogComponent, + SafeHtmlPipe + ], + templateUrl: './out-of-date-modal.component.html', + styleUrl: './out-of-date-modal.component.scss' +}) +export class OutOfDateModalComponent { + + private readonly ngbModal = inject(NgbActiveModal); + + @Input({required: true}) versionsOutOfDate: number = 0; + + close() { + this.ngbModal.close(); + } +} diff --git a/UI/Web/src/app/app.component.ts b/UI/Web/src/app/app.component.ts index 351e2ff10..941153732 100644 --- a/UI/Web/src/app/app.component.ts +++ b/UI/Web/src/app/app.component.ts @@ -1,17 +1,20 @@ import {ChangeDetectorRef, Component, DestroyRef, HostListener, inject, Inject, OnInit} from '@angular/core'; import {NavigationStart, Router, RouterOutlet} from '@angular/router'; -import {map, shareReplay, take} from 'rxjs/operators'; +import {map, shareReplay, take, tap} from 'rxjs/operators'; import { AccountService } from './_services/account.service'; import { LibraryService } from './_services/library.service'; import { NavService } from './_services/nav.service'; import { filter } from 'rxjs/operators'; import {NgbModal, NgbModalConfig, NgbOffcanvas, NgbRatingConfig} from '@ng-bootstrap/ng-bootstrap'; import { DOCUMENT, NgClass, NgIf, AsyncPipe } from '@angular/common'; -import { Observable } from 'rxjs'; +import {interval, Observable, switchMap} from 'rxjs'; import {ThemeService} from "./_services/theme.service"; import { SideNavComponent } from './sidenav/_components/side-nav/side-nav.component'; import {NavHeaderComponent} from "./nav/_components/nav-header/nav-header.component"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; +import {ServerService} from "./_services/server.service"; +import {ImportCblModalComponent} from "./reading-list/_modals/import-cbl-modal/import-cbl-modal.component"; +import {OutOfDateModalComponent} from "./announcements/_components/out-of-date-modal/out-of-date-modal.component"; @Component({ selector: 'app-root', @@ -28,6 +31,7 @@ export class AppComponent implements OnInit { private readonly offcanvas = inject(NgbOffcanvas); public readonly navService = inject(NavService); public readonly cdRef = inject(ChangeDetectorRef); + public readonly serverService = inject(ServerService); constructor(private accountService: AccountService, private libraryService: LibraryService, @@ -66,7 +70,11 @@ export class AppComponent implements OnInit { }); - this.transitionState$ = this.accountService.currentUser$.pipe(map((user) => { + this.transitionState$ = this.accountService.currentUser$.pipe( + tap(user => { + + }), + map((user) => { if (!user) return false; return user.preferences.noTransitions; }), takeUntilDestroyed(this.destroyRef)); @@ -93,6 +101,26 @@ export class AppComponent implements OnInit { // Bootstrap anything that's needed this.themeService.getThemes().subscribe(); this.libraryService.getLibraryNames().pipe(take(1), shareReplay({refCount: true, bufferSize: 1})).subscribe(); + // On load, make an initial call for valid license + this.accountService.hasValidLicense().subscribe(); + + // Every hour, have the UI check for an update. People seriously stay out of date + interval(2* 60 * 60 * 1000) // 2 hours in milliseconds + .pipe( + switchMap(() => this.accountService.currentUser$), + filter(u => u !== undefined && this.accountService.hasAdminRole(u)), + switchMap(_ => this.serverService.checkHowOutOfDate()), + filter(versionOutOfDate => { + return !isNaN(versionOutOfDate) && versionOutOfDate > 2; + }), + tap(versionOutOfDate => { + if (!this.ngbModal.hasOpenModals()) { + const ref = this.ngbModal.open(OutOfDateModalComponent, {size: 'xl', fullscreen: 'md'}); + ref.componentInstance.versionsOutOfDate = 3; + } + }) + ) + .subscribe(); } } } diff --git a/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.ts b/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.ts index 73b0fe1bc..9a02dd58b 100644 --- a/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.ts +++ b/UI/Web/src/app/book-reader/_components/book-line-overlay/book-line-overlay.component.ts @@ -84,12 +84,19 @@ export class BookLineOverlayComponent implements OnInit { const selection = window.getSelection(); if (!event.target) return; + + if ((selection === null || selection === undefined || selection.toString().trim() === '' || selection.toString().trim() === this.selectedText)) { if (this.selectedText !== '') { event.preventDefault(); event.stopPropagation(); } - this.reset(); + + const isRightClick = (event instanceof MouseEvent && event.button === 2); + if (!isRightClick) { + this.reset(); + } + return; } diff --git a/UI/Web/src/app/bookmark/_components/bookmarks/bookmarks.component.html b/UI/Web/src/app/bookmark/_components/bookmarks/bookmarks.component.html index df44e5cc6..721c879c4 100644 --- a/UI/Web/src/app/bookmark/_components/bookmarks/bookmarks.component.html +++ b/UI/Web/src/app/bookmark/_components/bookmarks/bookmarks.component.html @@ -19,7 +19,8 @@ diff --git a/UI/Web/src/app/bookmark/_components/bookmarks/bookmarks.component.ts b/UI/Web/src/app/bookmark/_components/bookmarks/bookmarks.component.ts index fbb46150b..2e72e01f1 100644 --- a/UI/Web/src/app/bookmark/_components/bookmarks/bookmarks.component.ts +++ b/UI/Web/src/app/bookmark/_components/bookmarks/bookmarks.component.ts @@ -9,11 +9,11 @@ import { } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { ToastrService } from 'ngx-toastr'; -import { take } from 'rxjs'; +import {take} from 'rxjs'; import { BulkSelectionService } from 'src/app/cards/bulk-selection.service'; import { FilterSettings } from 'src/app/metadata-filter/filter-settings'; import { ConfirmService } from 'src/app/shared/confirm.service'; -import { DownloadService } from 'src/app/shared/_services/download.service'; +import {DownloadService} from 'src/app/shared/_services/download.service'; import { FilterUtilitiesService } from 'src/app/shared/_services/filter-utilities.service'; import { KEY_CODES } from 'src/app/shared/_services/utility.service'; import { JumpKey } from 'src/app/_models/jumpbar/jump-key'; @@ -25,7 +25,6 @@ import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/acti import { ImageService } from 'src/app/_services/image.service'; import { JumpbarService } from 'src/app/_services/jumpbar.service'; import { ReaderService } from 'src/app/_services/reader.service'; -import { SeriesService } from 'src/app/_services/series.service'; import {DecimalPipe, NgIf} from '@angular/common'; import { CardItemComponent } from '../../../cards/card-item/card-item.component'; import { CardDetailLayoutComponent } from '../../../cards/card-detail-layout/card-detail-layout.component'; @@ -45,11 +44,25 @@ import {Title} from "@angular/platform-browser"; }) export class BookmarksComponent implements OnInit { + private readonly translocoService = inject(TranslocoService); + private readonly readerService = inject(ReaderService); + private readonly downloadService = inject(DownloadService); + private readonly toastr = inject(ToastrService); + private readonly confirmService = inject(ConfirmService); + private readonly actionFactoryService = inject(ActionFactoryService); + private readonly router = inject(Router); + private readonly cdRef = inject(ChangeDetectorRef); + private readonly filterUtilityService = inject(FilterUtilitiesService); + private readonly route = inject(ActivatedRoute); + private readonly jumpbarService = inject(JumpbarService); + private readonly titleService = inject(Title); + public readonly bulkSelectionService = inject(BulkSelectionService); + public readonly imageService = inject(ImageService); + bookmarks: Array = []; series: Array = []; loadingBookmarks: boolean = false; seriesIds: {[id: number]: number} = {}; - downloadingSeries: {[id: number]: boolean} = {}; clearingSeries: {[id: number]: boolean} = {}; actions: ActionItem[] = []; jumpbarKeys: Array = []; @@ -64,16 +77,7 @@ export class BookmarksComponent implements OnInit { trackByIdentity = (index: number, item: Series) => `${item.name}_${item.localizedName}_${item.pagesRead}`; refresh: EventEmitter = new EventEmitter(); - private readonly translocoService = inject(TranslocoService); - - constructor(private readerService: ReaderService, - private downloadService: DownloadService, private toastr: ToastrService, - private confirmService: ConfirmService, public bulkSelectionService: BulkSelectionService, - public imageService: ImageService, private actionFactoryService: ActionFactoryService, - private router: Router, private readonly cdRef: ChangeDetectorRef, - private filterUtilityService: FilterUtilitiesService, private route: ActivatedRoute, - private jumpbarService: JumpbarService, private titleService: Title) { - + constructor() { this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot).subscribe(filter => { this.filter = filter; @@ -160,8 +164,11 @@ export class BookmarksComponent implements OnInit { this.readerService.getAllBookmarks(this.filter).pipe(take(1)).subscribe(bookmarks => { this.bookmarks = bookmarks; this.bookmarks.forEach(bmk => { - this.downloadingSeries[bmk.seriesId] = false; this.clearingSeries[bmk.seriesId] = false; + if (!this.seriesIds.hasOwnProperty(bmk.seriesId)) { + this.seriesIds[bmk.seriesId] = 0; + } + this.seriesIds[bmk.seriesId] += 1; }); const distinctSeriesMap = new Map(); @@ -199,14 +206,7 @@ export class BookmarksComponent implements OnInit { } downloadBookmarks(series: Series) { - this.downloadingSeries[series.id] = true; - this.cdRef.markForCheck(); - this.downloadService.download('bookmark', this.bookmarks.filter(bmk => bmk.seriesId === series.id), (d) => { - if (!d) { - this.downloadingSeries[series.id] = false; - this.cdRef.markForCheck(); - } - }); + this.downloadService.download('bookmark', this.bookmarks.filter(bmk => bmk.seriesId === series.id)); } updateFilter(data: FilterEvent) { diff --git a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html index 3a3b33907..839198bc4 100644 --- a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html +++ b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html @@ -30,10 +30,9 @@
-
+
- +
{{t('required-field')}} @@ -461,6 +460,18 @@
diff --git a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts index 379b68521..8d324e497 100644 --- a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts +++ b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts @@ -51,10 +51,13 @@ import {PublicationStatusPipe} from "../../../_pipes/publication-status.pipe"; import {BytesPipe} from "../../../_pipes/bytes.pipe"; import {ImageComponent} from "../../../shared/image/image.component"; import {DefaultValuePipe} from "../../../_pipes/default-value.pipe"; -import {TranslocoModule} from "@ngneat/transloco"; +import {translate, TranslocoModule} from "@ngneat/transloco"; import {TranslocoDatePipe} from "@ngneat/transloco-locale"; import {UtcToLocalTimePipe} from "../../../_pipes/utc-to-local-time.pipe"; import {EditListComponent} from "../../../shared/edit-list/edit-list.component"; +import {AccountService} from "../../../_services/account.service"; +import {LibraryType} from "../../../_models/library/library"; +import {ToastrService} from "ngx-toastr"; enum TabID { General = 0, @@ -66,6 +69,13 @@ enum TabID { Info = 6, } +export interface EditSeriesModalCloseResult { + success: boolean; + series: Series; + coverImageUpdate: boolean; + updateExternal: boolean +} + @Component({ selector: 'app-edit-series-modal', standalone: true, @@ -112,6 +122,9 @@ export class EditSeriesModalComponent implements OnInit { private readonly uploadService = inject(UploadService); private readonly metadataService = inject(MetadataService); private readonly cdRef = inject(ChangeDetectorRef); + public readonly accountService = inject(AccountService); + private readonly destroyRef = inject(DestroyRef); + private readonly toastr = inject(ToastrService); protected readonly TabID = TabID; protected readonly PersonRole = PersonRole; @@ -133,7 +146,9 @@ export class EditSeriesModalComponent implements OnInit { editSeriesForm!: FormGroup; libraryName: string | undefined = undefined; size: number = 0; - private readonly destroyRef = inject(DestroyRef); + hasForcedKPlus = false; + forceIsLoading = false; + // Typeaheads tagsSettings: TypeaheadSettings = new TypeaheadSettings(); @@ -440,10 +455,6 @@ export class EditSeriesModalComponent implements OnInit { return a.isoCode == b.isoCode; } - if (this.metadata.language === undefined || this.metadata.language === null || this.metadata.language === '') { - this.metadata.language = 'en'; - } - const l = this.validLanguages.find(l => l.isoCode === this.metadata.language); if (l !== undefined) { this.languageSettings.savedData = l; @@ -506,7 +517,17 @@ export class EditSeriesModalComponent implements OnInit { } close() { - this.modal.close({success: false, series: undefined, coverImageUpdate: this.coverImageReset}); + this.modal.close({success: false, series: undefined, coverImageUpdate: this.coverImageReset, updateExternal: this.hasForcedKPlus}); + } + + forceScan() { + this.forceIsLoading = true; + this.metadataService.forceRefreshFromPlus(this.series.id).subscribe(() => { + this.hasForcedKPlus = true; + this.forceIsLoading = false; + this.toastr.info(translate('toasts.force-kavita+-refresh-success')); + this.cdRef.markForCheck(); + }); } fetchCollectionTags(filter: string = '') { @@ -545,7 +566,7 @@ export class EditSeriesModalComponent implements OnInit { this.saveNestedComponents.emit(); forkJoin(apis).subscribe(results => { - this.modal.close({success: true, series: model, coverImageUpdate: selectedIndex > 0 || this.coverImageReset}); + this.modal.close({success: true, series: model, coverImageUpdate: selectedIndex > 0 || this.coverImageReset, updateExternal: this.hasForcedKPlus}); }); } diff --git a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.html b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.html index 7852c8331..183d56e19 100644 --- a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.html +++ b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.html @@ -108,45 +108,52 @@
  • {{t(tabs[TabID.Files].title)}} -

    {{utilityService.formatChapterName(libraryType) + 's'}}

    + @if (!utilityService.isChapter(data)) { +

    {{utilityService.formatChapterName(libraryType) + 's'}}

    + }
    • +
      - - - - - {{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}} - - - - {{chapter.pagesRead}} / {{chapter.pages}} - {{t('unread') | uppercase}} - {{t('read') | uppercase}} - - + + + + + {{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}} + + + + {{chapter.pagesRead}} / {{chapter.pages}} + {{t('unread') | uppercase}} + {{t('read') | uppercase}} + + {{t('files')}}
        -
      • - {{file.filePath}} -
        -
        - {{t('pages')}} {{file.pages | number:''}} + @for (file of chapter.files; track file.id) { +
      • + {{file.filePath}} +
        +
        + {{t('pages')}} {{file.pages | number:''}} +
        + @if (data.hasOwnProperty('created')) { +
        + {{t('added')}} {{file.created | date: 'short' | defaultDate}} +
        + } +
        + {{t('size')}} {{file.bytes | bytes}} +
        -
        - {{t('added')}} {{file.created | date: 'short' | defaultDate}} -
        -
        - {{t('size')}} {{file.bytes | bytes}} -
        -
      -
    • + + }
  • diff --git a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts index 83e48cfc7..d4a4a85e6 100644 --- a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts +++ b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts @@ -68,12 +68,17 @@ enum TabID { }) export class CardDetailDrawerComponent implements OnInit { + private readonly destroyRef = inject(DestroyRef); + + protected readonly MangaFormat = MangaFormat; + protected readonly Breakpoint = Breakpoint; + protected readonly LibraryType = LibraryType; + protected readonly TabID = TabID; + @Input() parentName = ''; @Input() seriesId: number = 0; @Input() libraryId: number = 0; @Input({required: true}) data!: Volume | Chapter; - private readonly destroyRef = inject(DestroyRef); - /** * If this is a volume, this will be first chapter for said volume. @@ -104,26 +109,13 @@ export class CardDetailDrawerComponent implements OnInit { ]; active = this.tabs[0]; - chapterMetadata!: ChapterMetadata; + chapterMetadata: ChapterMetadata | undefined; summary: string = ''; downloadInProgress: boolean = false; - get MangaFormat() { - return MangaFormat; - } - get Breakpoint() { - return Breakpoint; - } - get LibraryType() { - return LibraryType; - } - - get TabID() { - return TabID; - } constructor(public utilityService: UtilityService, public imageService: ImageService, private uploadService: UploadService, private toastr: ToastrService, @@ -160,7 +152,7 @@ export class CardDetailDrawerComponent implements OnInit { this.chapterActions = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this)) .filter(item => item.action !== Action.Edit); - this.chapterActions.push({title: 'Read', action: Action.Read, callback: this.handleChapterActionCallback.bind(this), requiresAdmin: false, children: []}); + this.chapterActions.push({title: 'read', action: Action.Read, callback: this.handleChapterActionCallback.bind(this), requiresAdmin: false, children: []}); if (this.isChapter) { const chapter = this.utilityService.asChapter(this.data); this.chapterActions = this.actionFactoryService.filterSendToAction(this.chapterActions, chapter); diff --git a/UI/Web/src/app/cards/card-item/card-item.component.ts b/UI/Web/src/app/cards/card-item/card-item.component.ts index d9f6ddbb3..b9d20e602 100644 --- a/UI/Web/src/app/cards/card-item/card-item.component.ts +++ b/UI/Web/src/app/cards/card-item/card-item.component.ts @@ -277,14 +277,8 @@ export class CardItemComponent implements OnInit { }); this.download$ = this.downloadService.activeDownloads$.pipe(takeUntilDestroyed(this.destroyRef), map((events) => { - if(this.utilityService.isSeries(this.entity)) return events.find(e => e.entityType === 'series' && e.subTitle === this.downloadService.downloadSubtitle('series', (this.entity as Series))) || null; - if(this.utilityService.isVolume(this.entity)) return events.find(e => e.entityType === 'volume' && e.subTitle === this.downloadService.downloadSubtitle('volume', (this.entity as Volume))) || null; - if(this.utilityService.isChapter(this.entity)) return events.find(e => e.entityType === 'chapter' && e.subTitle === this.downloadService.downloadSubtitle('chapter', (this.entity as Chapter))) || null; - // Is PageBookmark[] - if(this.entity.hasOwnProperty('length')) return events.find(e => e.entityType === 'bookmark' && e.subTitle === this.downloadService.downloadSubtitle('bookmark', [(this.entity as PageBookmark)])) || null; - return null; + return this.downloadService.mapToEntityType(events, this.entity); })); - } diff --git a/UI/Web/src/app/cards/download-indicator/download-indicator.component.ts b/UI/Web/src/app/cards/download-indicator/download-indicator.component.ts index 418655d6a..aeb541a67 100644 --- a/UI/Web/src/app/cards/download-indicator/download-indicator.component.ts +++ b/UI/Web/src/app/cards/download-indicator/download-indicator.component.ts @@ -20,6 +20,4 @@ export class DownloadIndicatorComponent { * Observable that represents when the download completes */ @Input({required: true}) download$!: Observable | null; - - constructor() { } } diff --git a/UI/Web/src/app/cards/entity-title/entity-title.component.html b/UI/Web/src/app/cards/entity-title/entity-title.component.html index b4700eb64..62730076c 100644 --- a/UI/Web/src/app/cards/entity-title/entity-title.component.html +++ b/UI/Web/src/app/cards/entity-title/entity-title.component.html @@ -7,9 +7,9 @@ {{seriesName.length > 0 ? seriesName + ' - ' : ''}} - {{entity.number !== 0 ? (isChapter && includeVolume ? volumeTitle : '') : ''}} + {{Number !== 0 ? (isChapter && includeVolume ? volumeTitle : '') : ''}} - {{entity.number !== 0 ? (isChapter ? t('issue-num') + entity.number : volumeTitle) : t('special')}} + {{Number !== 0 ? (isChapter ? t('issue-num') + Number : volumeTitle) : t('special')}} @@ -19,13 +19,16 @@ {{seriesName.length > 0 ? seriesName + ' - ' : ''}} - {{entity.number !== 0 ? (isChapter && includeVolume ? volumeTitle : '') : ''}} + {{Number !== 0 ? (isChapter && includeVolume ? volumeTitle : '') : ''}} - {{entity.number !== 0 ? (isChapter ? (t('chapter') + ' ') + entity.number : volumeTitle) : t('special')}} + {{Number !== 0 ? (isChapter ? (t('chapter') + ' ') + Number : volumeTitle) : t('special')}} {{volumeTitle}} + + {{volumeTitle}} + diff --git a/UI/Web/src/app/cards/entity-title/entity-title.component.ts b/UI/Web/src/app/cards/entity-title/entity-title.component.ts index 13c7f4bb8..564c5eaaa 100644 --- a/UI/Web/src/app/cards/entity-title/entity-title.component.ts +++ b/UI/Web/src/app/cards/entity-title/entity-title.component.ts @@ -39,6 +39,10 @@ export class EntityTitleComponent implements OnInit { titleName: string = ''; volumeTitle: string = ''; + get Number() { + if (this.utilityService.isVolume(this.entity)) return (this.entity as Volume).minNumber; + return (this.entity as Chapter).number; + } get LibraryType() { return LibraryType; diff --git a/UI/Web/src/app/carousel/_components/carousel-reel/carousel-reel.component.html b/UI/Web/src/app/carousel/_components/carousel-reel/carousel-reel.component.html index c2104bf98..8e5d7c45c 100644 --- a/UI/Web/src/app/carousel/_components/carousel-reel/carousel-reel.component.html +++ b/UI/Web/src/app/carousel/_components/carousel-reel/carousel-reel.component.html @@ -8,8 +8,8 @@
    - - + +
    @if (items.length > 0) { diff --git a/UI/Web/src/app/carousel/_components/carousel-reel/carousel-reel.component.scss b/UI/Web/src/app/carousel/_components/carousel-reel/carousel-reel.component.scss index 54b177277..4de45c5b8 100644 --- a/UI/Web/src/app/carousel/_components/carousel-reel/carousel-reel.component.scss +++ b/UI/Web/src/app/carousel/_components/carousel-reel/carousel-reel.component.scss @@ -21,6 +21,10 @@ .non-selectable { cursor: default; } + + .carousel-btn > i { + color: var(--carousel-btn-color); + } } diff --git a/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts b/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts index af026d3e6..83bfaddc2 100644 --- a/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts +++ b/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts @@ -47,7 +47,7 @@ import { SideNavCompanionBarComponent } from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component'; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; -import {TranslocoDirective, TranslocoService} from "@ngneat/transloco"; +import {translate, TranslocoDirective, TranslocoService} from "@ngneat/transloco"; import {CardActionablesComponent} from "../../../_single-module/card-actionables/card-actionables.component"; import {FilterField} from "../../../_models/metadata/v2/filter-field"; import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison"; @@ -266,6 +266,12 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked { case(Action.Edit): this.openEditCollectionTagModal(this.collectionTag); break; + case (Action.Delete): + this.collectionService.deleteTag(this.collectionTag.id).subscribe(() => { + this.toastr.success(translate('toasts.collection-tag-deleted')); + this.router.navigateByUrl('collections'); + }); + break; default: break; } diff --git a/UI/Web/src/app/dashboard/_components/dashboard.component.html b/UI/Web/src/app/dashboard/_components/dashboard.component.html index a455bb5a9..47a2cb9d3 100644 --- a/UI/Web/src/app/dashboard/_components/dashboard.component.html +++ b/UI/Web/src/app/dashboard/_components/dashboard.component.html @@ -1,86 +1,98 @@ - - -
    -
    -

    {{t('no-libraries')}} {{t('server-settings-link')}}

    -
    -
    -

    {{t('not-granted')}}

    -
    -
    -
    -
    + @if (libraries$ | async; as libraries) { + @if (libraries.length === 0) { + @if (isAdmin$ | async; as isAdmin) { +
    + @if (isAdmin) { +
    +

    {{t('no-libraries')}} {{t('server-settings-link')}}

    +
    + } @else { +
    +

    {{t('not-granted')}}

    +
    + } +
    + } + } + } - - - + @for(stream of streams; track stream.id) { + @switch (stream.streamType) { + @case (StreamType.OnDeck) { + + } + @case (StreamType.RecentlyUpdated) { + + } + @case (StreamType.NewlyAdded) { + + } + @case (StreamType.SmartFilter) { + + } + @case (StreamType.MoreInGenre) { + + } + } - - - - - - - - - + @if(stream.api | async; as data) { - + } - - + @if(stream.api | async; as data) { + + (reload)="reloadStream(stream.id, true)" (dataChanged)="reloadStream(stream.id)"> - + } - - + @if(stream.api | async; as data) { + - + } - - + @if(stream.api | async; as data) { + - + } - - + @if(stream.api | async; as data) { + - + } - - + }
    + diff --git a/UI/Web/src/app/dashboard/_components/dashboard.component.ts b/UI/Web/src/app/dashboard/_components/dashboard.component.ts index c186e32f1..5da160d30 100644 --- a/UI/Web/src/app/dashboard/_components/dashboard.component.ts +++ b/UI/Web/src/app/dashboard/_components/dashboard.component.ts @@ -1,4 +1,4 @@ -import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, Input, OnInit} from '@angular/core'; +import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core'; import {Title} from '@angular/platform-browser'; import {Router, RouterLink} from '@angular/router'; import {Observable, of, ReplaySubject, Subject, switchMap} from 'rxjs'; @@ -16,7 +16,7 @@ import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {CardItemComponent} from '../../cards/card-item/card-item.component'; import {SeriesCardComponent} from '../../cards/series-card/series-card.component'; import {CarouselReelComponent} from '../../carousel/_components/carousel-reel/carousel-reel.component'; -import {AsyncPipe, NgForOf, NgIf, NgSwitch, NgSwitchCase, NgTemplateOutlet} from '@angular/common'; +import {AsyncPipe, NgForOf, NgTemplateOutlet} from '@angular/common'; import { SideNavCompanionBarComponent } from '../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component'; @@ -30,6 +30,17 @@ import {Genre} from "../../_models/metadata/genre"; import {DashboardStream} from "../../_models/dashboard/dashboard-stream"; import {StreamType} from "../../_models/dashboard/stream-type.enum"; import {LoadingComponent} from "../../shared/loading/loading.component"; +import {ScrobbleProvider, ScrobblingService} from "../../_services/scrobbling.service"; +import {ToastrService} from "ngx-toastr"; +import {ServerService} from "../../_services/server.service"; + +enum StreamId { + OnDeck, + RecentlyUpdatedSeries, + NewlyAddedSeries, + MoreInGenre, +} + @Component({ selector: 'app-dashboard', @@ -37,8 +48,8 @@ import {LoadingComponent} from "../../shared/loading/loading.component"; styleUrls: ['./dashboard.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [SideNavCompanionBarComponent, NgIf, RouterLink, CarouselReelComponent, SeriesCardComponent, - CardItemComponent, AsyncPipe, TranslocoDirective, NgSwitchCase, NgSwitch, NgForOf, NgTemplateOutlet, LoadingComponent], + imports: [SideNavCompanionBarComponent, RouterLink, CarouselReelComponent, SeriesCardComponent, + CardItemComponent, AsyncPipe, TranslocoDirective, NgForOf, NgTemplateOutlet, LoadingComponent], }) export class DashboardComponent implements OnInit { @@ -55,6 +66,9 @@ export class DashboardComponent implements OnInit { private readonly messageHub = inject(MessageHubService); private readonly cdRef = inject(ChangeDetectorRef); private readonly dashboardService = inject(DashboardService); + private readonly scrobblingService = inject(ScrobblingService); + private readonly toastr = inject(ToastrService); + private readonly serverService = inject(ServerService); libraries$: Observable = this.libraryService.getLibraries().pipe(take(1), takeUntilDestroyed(this.destroyRef)) isLoadingDashboard = true; @@ -73,6 +87,7 @@ export class DashboardComponent implements OnInit { */ private loadRecentlyAdded$: ReplaySubject = new ReplaySubject(); protected readonly StreamType = StreamType; + protected readonly StreamId = StreamId; constructor() { this.loadDashboard(); @@ -105,6 +120,14 @@ export class DashboardComponent implements OnInit { } }); + this.scrobblingService.hasTokenExpired(ScrobbleProvider.AniList).subscribe(hasExpired => { + if (hasExpired) { + this.toastr.error(translate('toasts.anilist-token-expired')); + } + this.cdRef.markForCheck(); + }); + + this.isAdmin$ = this.accountService.currentUser$.pipe( takeUntilDestroyed(this.destroyRef), map(user => (user && this.accountService.hasAdminRole(user)) || false), @@ -170,12 +193,17 @@ export class DashboardComponent implements OnInit { this.cdRef.markForCheck(); } - reloadStream(streamId: number) { + reloadStream(streamId: number, onDeck = false) { const index = this.streams.findIndex(s => s.id === streamId); if (index < 0) return; - this.streams[index] = {...this.streams[index]}; - console.log('swapped out stream: ', this.streams[index]); - this.cdRef.detectChanges(); + if (onDeck) { + // TODO: Need to figure out a better way to refresh just one stream + this.refreshStreams$.next(); + this.cdRef.markForCheck(); + } else { + this.streams[index] = {...this.streams[index]}; + this.cdRef.markForCheck(); + } } async handleRecentlyAddedChapterClick(item: RecentlyAddedItem) { @@ -186,18 +214,18 @@ export class DashboardComponent implements OnInit { await this.router.navigateByUrl('all-series?' + stream.smartFilterEncoded); } - handleSectionClick(sectionTitle: string) { - if (sectionTitle.toLowerCase() === 'recently updated series') { + handleSectionClick(streamId: StreamId) { + if (streamId === StreamId.RecentlyUpdatedSeries) { const params: any = {}; params['page'] = 1; - params['title'] = 'Recently Updated'; + params['title'] = translate('dashboard.recently-updated-title'); const filter = this.filterUtilityService.createSeriesV2Filter(); if (filter.sortOptions) { filter.sortOptions.sortField = SortField.LastChapterAdded; filter.sortOptions.isAscending = false; } this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params).subscribe(); - } else if (sectionTitle.toLowerCase() === 'on deck') { + } else if (streamId === StreamId.OnDeck) { const params: any = {}; params['page'] = 1; params['title'] = translate('dashboard.on-deck-title'); @@ -210,7 +238,7 @@ export class DashboardComponent implements OnInit { filter.sortOptions.isAscending = false; } this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params).subscribe(); - } else if (sectionTitle.toLowerCase() === 'newly added series') { + } else if (streamId === StreamId.NewlyAddedSeries) { const params: any = {}; params['page'] = 1; params['title'] = translate('dashboard.recently-added-title'); @@ -220,10 +248,10 @@ export class DashboardComponent implements OnInit { filter.sortOptions.isAscending = false; } this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params).subscribe(); - } else if (sectionTitle.toLowerCase() === 'more in genre') { + } else if (streamId === StreamId.MoreInGenre) { const params: any = {}; params['page'] = 1; - params['title'] = translate('more-in-genre-title', {genre: this.genre?.title}); + params['title'] = translate('dashboard.more-in-genre-title', {genre: this.genre?.title}); const filter = this.filterUtilityService.createSeriesV2Filter(); filter.statements.push({field: FilterField.Genres, value: this.genre?.id + '', comparison: FilterComparison.MustContains}); this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params).subscribe(); diff --git a/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.html b/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.html index ceba55c95..f80179327 100644 --- a/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.html +++ b/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.html @@ -13,7 +13,7 @@ Scroll Top: {{getScrollTop()}}
    - -
    - -
    + + @if (seriesMetadata) { +
    + +
    + } +
    -
    +
    - +
    @@ -137,7 +153,7 @@
    - - + + - + - + @@ -24,14 +24,17 @@ + {{item.title}} + {{item.title}} + @@ -40,7 +43,7 @@ - + @@ -53,6 +56,16 @@ + + + + + + + + + + @@ -62,70 +75,95 @@
    + + + + + + + + + + + - - - - - - - - - - + + + + + - - - - - - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    - + diff --git a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts index 374c866a6..e3fd490e3 100644 --- a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts @@ -9,7 +9,6 @@ import { ViewEncapsulation } from '@angular/core'; import {Router} from '@angular/router'; -import {ReaderService} from 'src/app/_services/reader.service'; import {TagBadgeComponent, TagBadgeCursor} from '../../../shared/tag-badge/tag-badge.component'; import {FilterUtilitiesService} from '../../../shared/_services/filter-utilities.service'; import {Breakpoint, UtilityService} from '../../../shared/_services/utility.service'; @@ -33,6 +32,7 @@ import {TranslocoDirective} from "@ngneat/transloco"; import {FilterField} from "../../../_models/metadata/v2/filter-field"; import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison"; import {ImageComponent} from "../../../shared/image/image.component"; +import {Rating} from "../../../_models/rating"; @Component({ @@ -48,6 +48,18 @@ import {ImageComponent} from "../../../shared/image/image.component"; }) export class SeriesMetadataDetailComponent implements OnChanges { + protected readonly imageService = inject(ImageService); + protected readonly utilityService = inject(UtilityService); + private readonly router = inject(Router); + private readonly cdRef = inject(ChangeDetectorRef); + private readonly filterUtilityService = inject(FilterUtilitiesService); + + protected readonly FilterField = FilterField; + protected readonly LibraryType = LibraryType; + protected readonly MangaFormat = MangaFormat; + protected readonly TagBadgeCursor = TagBadgeCursor; + protected readonly Breakpoint = Breakpoint; + @Input({required: true}) seriesMetadata!: SeriesMetadata; @Input({required: true}) libraryType!: LibraryType; @Input() hasReadingProgress: boolean = false; @@ -56,27 +68,16 @@ export class SeriesMetadataDetailComponent implements OnChanges { */ @Input() readingLists: Array = []; @Input({required: true}) series!: Series; + @Input({required: true}) ratings: Array = []; isCollapsed: boolean = true; hasExtendedProperties: boolean = false; - protected readonly imageService = inject(ImageService); - protected readonly utilityService = inject(UtilityService); - private readonly router = inject(Router); - private readonly readerService = inject(ReaderService); - private readonly cdRef = inject(ChangeDetectorRef); - private readonly filterUtilityService = inject(FilterUtilitiesService); - /** * Html representation of Series Summary */ seriesSummary: string = ''; - protected FilterField = FilterField; - protected LibraryType = LibraryType; - protected MangaFormat = MangaFormat; - protected TagBadgeCursor = TagBadgeCursor; - get WebLinks() { if (this.seriesMetadata?.webLinks === '') return []; return this.seriesMetadata?.webLinks.split(',') || []; @@ -121,6 +122,4 @@ export class SeriesMetadataDetailComponent implements OnChanges { navigate(basePage: string, id: number) { this.router.navigate([basePage, id]); } - - protected readonly Breakpoint = Breakpoint; } diff --git a/UI/Web/src/app/shared/_services/download.service.ts b/UI/Web/src/app/shared/_services/download.service.ts index 20c44046e..0e12a9660 100644 --- a/UI/Web/src/app/shared/_services/download.service.ts +++ b/UI/Web/src/app/shared/_services/download.service.ts @@ -22,6 +22,10 @@ import { BytesPipe } from 'src/app/_pipes/bytes.pipe'; import {translate} from "@ngneat/transloco"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {SAVER, Saver} from "../../_providers/saver.provider"; +import {UtilityService} from "./utility.service"; +import {CollectionTag} from "../../_models/collection-tag"; +import {RecentlyAddedItem} from "../../_models/recently-added-item"; +import {NextExpectedChapter} from "../../_models/series-detail/next-expected-chapter"; export const DEBOUNCE_TIME = 100; @@ -40,6 +44,10 @@ export interface DownloadEvent { * Progress of the download itself */ progress: number; + /** + * Entity id. For entities without id like logs or bookmarks, uses 0 instead + */ + id: number; } /** @@ -51,6 +59,7 @@ export type DownloadEntityType = 'volume' | 'chapter' | 'series' | 'bookmark' | */ export type DownloadEntity = Series | Volume | Chapter | PageBookmark[] | undefined; +export type QueueableDownloadType = Chapter | Volume; @Injectable({ providedIn: 'root' @@ -62,16 +71,39 @@ export class DownloadService { * Size in bytes in which to inform the user for confirmation before download starts. Defaults to 100 MB. */ public SIZE_WARNING = 104_857_600; + /** + * Sie in bytes in which to inform the user that anything above may fail on iOS due to device limits. (200MB) + */ + private IOS_SIZE_WARNING = 209_715_200; private downloadsSource: BehaviorSubject = new BehaviorSubject([]); + /** + * Active Downloads + */ public activeDownloads$ = this.downloadsSource.asObservable(); + private downloadQueue: BehaviorSubject = new BehaviorSubject([]); + /** + * Queued Downloads + */ + public queuedDownloads$ = this.downloadQueue.asObservable(); + private readonly destroyRef = inject(DestroyRef); private readonly confirmService = inject(ConfirmService); private readonly accountService = inject(AccountService); private readonly httpClient = inject(HttpClient); + private readonly utilityService = inject(UtilityService); - constructor(@Inject(SAVER) private save: Saver) { } + constructor(@Inject(SAVER) private save: Saver) { + this.downloadQueue.subscribe((queue) => { + if (queue.length > 0) { + const entity = queue.shift(); + console.log('Download Queue shifting entity: ', entity); + if (entity === undefined) return; + this.processDownload(entity); + } + }); + } /** @@ -80,12 +112,12 @@ export class DownloadService { * @param downloadEntity * @returns */ - downloadSubtitle(downloadEntityType: DownloadEntityType, downloadEntity: DownloadEntity | undefined) { + downloadSubtitle(downloadEntityType: DownloadEntityType | undefined, downloadEntity: DownloadEntity | undefined) { switch (downloadEntityType) { case 'series': return (downloadEntity as Series).name; case 'volume': - return (downloadEntity as Volume).number + ''; + return (downloadEntity as Volume).minNumber + ''; case 'chapter': return (downloadEntity as Chapter).number; case 'bookmark': @@ -93,6 +125,7 @@ export class DownloadService { case 'logs': return ''; } + return ''; } /** @@ -113,10 +146,12 @@ export class DownloadService { case 'volume': sizeCheckCall = this.downloadVolumeSize((entity as Volume).id); downloadCall = this.downloadVolume(entity as Volume); + //this.enqueueDownload(entity as Volume); break; case 'chapter': sizeCheckCall = this.downloadChapterSize((entity as Chapter).id); downloadCall = this.downloadChapter(entity as Chapter); + //this.enqueueDownload(entity as Chapter); break; case 'bookmark': sizeCheckCall = of(0); @@ -141,8 +176,10 @@ export class DownloadService { }) ).pipe(filter(wantsToDownload => { return wantsToDownload; - }), switchMap(() => { - return downloadCall.pipe( + }), + filter(_ => downloadCall !== undefined), + switchMap(() => { + return (downloadCall || of(undefined)).pipe( tap((d) => { if (callback) callback(d); }), @@ -178,12 +215,45 @@ export class DownloadService { download((blob, filename) => { this.save(blob, decodeURIComponent(filename)); }), - tap((d) => this.updateDownloadState(d, downloadType, subtitle)), + tap((d) => this.updateDownloadState(d, downloadType, subtitle, 0)), finalize(() => this.finalizeDownloadState(downloadType, subtitle)) ); } + private getIdKey(entity: Chapter | Volume) { + if (this.utilityService.isVolume(entity)) return 'volumeId'; + if (this.utilityService.isChapter(entity)) return 'chapterId'; + if (this.utilityService.isSeries(entity)) return 'seriesId'; + return 'id'; + } + + private getDownloadEntityType(entity: Chapter | Volume): DownloadEntityType { + if (this.utilityService.isVolume(entity)) return 'volume'; + if (this.utilityService.isChapter(entity)) return 'chapter'; + if (this.utilityService.isSeries(entity)) return 'series'; + return 'logs'; // This is a hack but it will never occur + } + + private downloadEntity(entity: Chapter | Volume): Observable { + const downloadEntityType = this.getDownloadEntityType(entity); + const subtitle = this.downloadSubtitle(downloadEntityType, entity); + const idKey = this.getIdKey(entity); + const url = `${this.baseUrl}download/${downloadEntityType}?${idKey}=${entity.id}`; + + return this.httpClient.get(url, { observe: 'events', responseType: 'blob', reportProgress: true }).pipe( + throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }), + download((blob, filename) => { + this.save(blob, decodeURIComponent(filename)); + }), + tap((d) => this.updateDownloadState(d, downloadEntityType, subtitle, entity.id)), + finalize(() => this.finalizeDownloadState(downloadEntityType, subtitle)) + ); + } + private downloadSeries(series: Series) { + + // TODO: Call backend for all the volumes and loose leaf chapters then enqueque them all + const downloadType = 'series'; const subtitle = this.downloadSubtitle(downloadType, series); return this.httpClient.get(this.baseUrl + 'download/series?seriesId=' + series.id, @@ -193,7 +263,7 @@ export class DownloadService { download((blob, filename) => { this.save(blob, decodeURIComponent(filename)); }), - tap((d) => this.updateDownloadState(d, downloadType, subtitle)), + tap((d) => this.updateDownloadState(d, downloadType, subtitle, series.id)), finalize(() => this.finalizeDownloadState(downloadType, subtitle)) ); } @@ -204,12 +274,12 @@ export class DownloadService { this.downloadsSource.next(values); } - private updateDownloadState(d: Download, entityType: DownloadEntityType, entitySubtitle: string) { + private updateDownloadState(d: Download, entityType: DownloadEntityType, entitySubtitle: string, id: number) { let values = this.downloadsSource.getValue(); if (d.state === 'PENDING') { const index = values.findIndex(v => v.entityType === entityType && v.subTitle === entitySubtitle); if (index >= 0) return; // Don't let us duplicate add - values.push({entityType: entityType, subTitle: entitySubtitle, progress: 0}); + values.push({entityType: entityType, subTitle: entitySubtitle, progress: 0, id}); } else if (d.state === 'IN_PROGRESS') { const index = values.findIndex(v => v.entityType === entityType && v.subTitle === entitySubtitle); if (index >= 0) { @@ -223,38 +293,19 @@ export class DownloadService { } private downloadChapter(chapter: Chapter) { - const downloadType = 'chapter'; - const subtitle = this.downloadSubtitle(downloadType, chapter); - return this.httpClient.get(this.baseUrl + 'download/chapter?chapterId=' + chapter.id, - {observe: 'events', responseType: 'blob', reportProgress: true} - ).pipe( - throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }), - download((blob, filename) => { - this.save(blob, decodeURIComponent(filename)); - }), - tap((d) => this.updateDownloadState(d, downloadType, subtitle)), - finalize(() => this.finalizeDownloadState(downloadType, subtitle)) - ); + return this.downloadEntity(chapter); } - private downloadVolume(volume: Volume): Observable { - const downloadType = 'volume'; - const subtitle = this.downloadSubtitle(downloadType, volume); - return this.httpClient.get(this.baseUrl + 'download/volume?volumeId=' + volume.id, - {observe: 'events', responseType: 'blob', reportProgress: true} - ).pipe( - throttleTime(DEBOUNCE_TIME, asyncScheduler, { leading: true, trailing: true }), - download((blob, filename) => { - this.save(blob, decodeURIComponent(filename)); - }), - tap((d) => this.updateDownloadState(d, downloadType, subtitle)), - finalize(() => this.finalizeDownloadState(downloadType, subtitle)) - ); + private downloadVolume(volume: Volume) { + return this.downloadEntity(volume); } private async confirmSize(size: number, entityType: DownloadEntityType) { + const showIosWarning = size > this.IOS_SIZE_WARNING && /iPad|iPhone|iPod/.test(navigator.userAgent); return (size < this.SIZE_WARNING || - await this.confirmService.confirm(translate('toasts.confirm-download-size', {entityType: 'entity-type.' + entityType, size: bytesPipe.transform(size)}))); + await this.confirmService.confirm(translate('toasts.confirm-download-size', + {entityType: translate('entity-type.' + entityType), size: bytesPipe.transform(size)}) + + (!showIosWarning ? '' : '

    ' + translate('toasts.confirm-download-size-ios')))); } private downloadBookmarks(bookmarks: PageBookmark[]) { @@ -268,8 +319,64 @@ export class DownloadService { download((blob, filename) => { this.save(blob, decodeURIComponent(filename)); }), - tap((d) => this.updateDownloadState(d, downloadType, subtitle)), + tap((d) => this.updateDownloadState(d, downloadType, subtitle, 0)), finalize(() => this.finalizeDownloadState(downloadType, subtitle)) ); } + + + + private processDownload(entity: QueueableDownloadType): void { + const downloadObservable = this.downloadEntity(entity); + console.log('Process Download called for entity: ', entity); + + // When we consume one, we need to take it off the queue + + downloadObservable.subscribe((downloadEvent) => { + // Download completed, process the next item in the queue + if (downloadEvent.state === 'DONE') { + this.processNextDownload(); + } + }); + } + + private processNextDownload(): void { + const currentQueue = this.downloadQueue.value; + if (currentQueue.length > 0) { + const nextEntity = currentQueue[0]; + this.processDownload(nextEntity); + } + } + + private enqueueDownload(entity: QueueableDownloadType): void { + const currentQueue = this.downloadQueue.value; + const newQueue = [...currentQueue, entity]; + this.downloadQueue.next(newQueue); + + // If the queue was empty, start processing the download + if (currentQueue.length === 0) { + this.processNextDownload(); + } + } + + mapToEntityType(events: DownloadEvent[], entity: Series | Volume | Chapter | CollectionTag | PageBookmark | RecentlyAddedItem | NextExpectedChapter) { + if(this.utilityService.isSeries(entity)) { + return events.find(e => e.entityType === 'series' && e.id == entity.id + && e.subTitle === this.downloadSubtitle('series', (entity as Series))) || null; + } + if(this.utilityService.isVolume(entity)) { + return events.find(e => e.entityType === 'volume' && e.id == entity.id + && e.subTitle === this.downloadSubtitle('volume', (entity as Volume))) || null; + } + if(this.utilityService.isChapter(entity)) { + return events.find(e => e.entityType === 'chapter' && e.id == entity.id + && e.subTitle === this.downloadSubtitle('chapter', (entity as Chapter))) || null; + } + // Is PageBookmark[] + if(entity.hasOwnProperty('length')) { + return events.find(e => e.entityType === 'bookmark' + && e.subTitle === this.downloadSubtitle('bookmark', [(entity as PageBookmark)])) || null; + } + return null; + } } diff --git a/UI/Web/src/app/shared/_services/utility.service.ts b/UI/Web/src/app/shared/_services/utility.service.ts index a6a3c146d..f6851fe42 100644 --- a/UI/Web/src/app/shared/_services/utility.service.ts +++ b/UI/Web/src/app/shared/_services/utility.service.ts @@ -65,6 +65,7 @@ export class UtilityService { switch(libraryType) { case LibraryType.Magazine: // TODO: Figure out if we need something special case LibraryType.Book: + case LibraryType.LightNovel: return this.translocoService.translate('common.book-num') + (includeSpace ? ' ' : ''); case LibraryType.Comic: if (includeHash) { diff --git a/UI/Web/src/app/shared/confirm.service.ts b/UI/Web/src/app/shared/confirm.service.ts index 48b7dbc2a..4cb3aa620 100644 --- a/UI/Web/src/app/shared/confirm.service.ts +++ b/UI/Web/src/app/shared/confirm.service.ts @@ -62,7 +62,7 @@ export class ConfirmService { config.content = content; } - const modalRef = this.modalService.open(ConfirmDialogComponent); + const modalRef = this.modalService.open(ConfirmDialogComponent, {size: "lg", fullscreen: "md"}); modalRef.componentInstance.config = config; modalRef.closed.pipe(take(1)).subscribe(result => { return resolve(result); diff --git a/UI/Web/src/app/shared/loading/loading.component.ts b/UI/Web/src/app/shared/loading/loading.component.ts index c153d8c5e..d45607e7e 100644 --- a/UI/Web/src/app/shared/loading/loading.component.ts +++ b/UI/Web/src/app/shared/loading/loading.component.ts @@ -19,6 +19,4 @@ export class LoadingComponent { * Uses absolute positioning to ensure it loads over content */ @Input() absolute: boolean = false; - - constructor() { } } diff --git a/UI/Web/src/app/sidenav/_components/side-nav-item/side-nav-item.component.scss b/UI/Web/src/app/sidenav/_components/side-nav-item/side-nav-item.component.scss index af9dc44aa..ea3e58a9e 100644 --- a/UI/Web/src/app/sidenav/_components/side-nav-item/side-nav-item.component.scss +++ b/UI/Web/src/app/sidenav/_components/side-nav-item/side-nav-item.component.scss @@ -100,7 +100,7 @@ a { text-decoration: none; - color: var(--side-nav-color); + color: var(--side-nav-text-color); } @media (max-width: 576px) { diff --git a/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.ts b/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.ts index a2d66c144..425e591f8 100644 --- a/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.ts +++ b/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.ts @@ -157,6 +157,9 @@ export class SideNavComponent implements OnInit { case (Action.AnalyzeFiles): await this.actionService.analyzeFiles(library); break; + case (Action.Delete): + await this.actionService.deleteLibrary(library); + break; case (Action.Edit): this.actionService.editLibrary(library, () => window.scrollTo(0, 0)); break; @@ -182,6 +185,7 @@ export class SideNavComponent implements OnInit { getLibraryTypeIcon(format: LibraryType) { switch (format) { case LibraryType.Book: + case LibraryType.LightNovel: return 'fa-book'; case LibraryType.Comic: case LibraryType.Manga: 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 f1442cf91..22884717e 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 @@ -2,7 +2,7 @@
    - + + + @if(IsKavitaPlusEligible) { + + {{t('kavitaplus-eligible-label')}} + + + } {{t('type-tooltip')}} + {{t('kavitaplus-eligible-tooltip')}} @@ -43,7 +51,7 @@
    {{t('last-scanned-label')}} - {{library.lastScanned | date: 'short' | defaultDate}} + {{library?.lastScanned | date: 'short' | defaultDate}}
    @@ -233,7 +241,7 @@