diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 50bfec06d..222f32903 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -12,7 +12,7 @@ jobs: - name: Checkout Repo uses: actions/checkout@v4 with: - fetch-depth: 0 + fetch-depth: 1 - name: Setup .NET Core uses: actions/setup-dotnet@v4 diff --git a/.github/workflows/build-ui.yml b/.github/workflows/build-ui.yml index 76f3dfb57..f227b3e69 100644 --- a/.github/workflows/build-ui.yml +++ b/.github/workflows/build-ui.yml @@ -12,7 +12,7 @@ jobs: - name: Checkout Repo uses: actions/checkout@v4 with: - fetch-depth: 0 + fetch-depth: 1 - name: NodeJS to Compile WebUI uses: actions/setup-node@v4 diff --git a/.gitignore b/.gitignore index 4de30b256..290dfcf79 100644 --- a/.gitignore +++ b/.gitignore @@ -485,64 +485,64 @@ Thumbs.db ssl/ # App specific -/API/kavita.db -/API/kavita.db-shm -/API/kavita.db-wal -/API/Hangfire.db -/API/Hangfire-log.db -API/cache/ -/API/wwwroot/ -/API/cache/ -/API/temp/ +/Kavita.Server/kavita.db +/Kavita.Server/kavita.db-shm +/Kavita.Server/kavita.db-wal +/Kavita.Server/Hangfire.db +/Kavita.Server/Hangfire-log.db +Kavita.Server/cache/ +/Kavita.Server/wwwroot/ +/Kavita.Server/cache/ +/Kavita.Server/temp/ _temp/ _output/ -API/stats/ +Kavita.Server/stats/ UI/Web/dist/ -/API.Tests/Extensions/Test Data/modified on run.txt +/Kavita.Services.Tests/Extensions/Test Data/modified on run.txt # All config files/folders in config except appsettings.json -/API/config-bak/ -/API/config-bak/*.* -/API/config-bak/**/ -/API/config/covers/ -/API/config/logs/ -/API/config/backups/ -/API/config/cache/ -/API/config/fonts/ -/API/config/temp/ -/API/config/themes/ -/API/config/stats/ -/API/config/bookmarks/ -/API/config/favicons/ -/API/config/cache-long/ -/API/config/*.db-shm -/API/config/*.db-wal -/API/config/*.db-journal -/API/config/*.db -/API/config/*.bak -/API/config/*.backup -/API/config/*.csv -/API/config/Hangfire.db -/API/config/Hangfire-log.db -API/config/covers/ -API/config/images/* -API/config/stats/* -API/config/stats/app_stats.json -API/config/pre-metadata/ -API/config/post-metadata/ -API/config/*.csv -API.Tests/TestResults/ +/Kavita.Server/config-bak/ +/Kavita.Server/config-bak/*.* +/Kavita.Server/config-bak/**/ +/Kavita.Server/config/covers/ +/Kavita.Server/config/logs/ +/Kavita.Server/config/backups/ +/Kavita.Server/config/cache/ +/Kavita.Server/config/fonts/ +/Kavita.Server/config/temp/ +/Kavita.Server/config/themes/ +/Kavita.Server/config/stats/ +/Kavita.Server/config/bookmarks/ +/Kavita.Server/config/favicons/ +/Kavita.Server/config/cache-long/ +/Kavita.Server/config/*.db-shm +/Kavita.Server/config/*.db-wal +/Kavita.Server/config/*.db-journal +/Kavita.Server/config/*.db +/Kavita.Server/config/*.bak +/Kavita.Server/config/*.backup +/Kavita.Server/config/*.csv +/Kavita.Server/config/Hangfire.db +/Kavita.Server/config/Hangfire-log.db +Kavita.Server/config/covers/ +Kavita.Server/config/images/* +Kavita.Server/config/stats/* +Kavita.Server/config/stats/app_stats.json +Kavita.Server/config/pre-metadata/ +Kavita.Server/config/post-metadata/ +Kavita.Server/config/*.csv +Kavita.Services.Tests/TestResults/ UI/Web/.vscode/settings.json -/API.Tests/Services/Test Data/ArchiveService/CoverImages/output/* +/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/output/* UI/Web/.angular/ BenchmarkDotNet.Artifacts .claude/ -API/config/*backup.zip +Kavita.Server/config/*backup.zip -API.Tests/Services/Test Data/ImageService/**/*_output* -API.Tests/Services/Test Data/ImageService/**/*_baseline* -API.Tests/Services/Test Data/ImageService/**/*.html +Kavita.Services.Tests/Test Data/ImageService/**/*_output*.* +Kavita.Services.Tests/Test Data/ImageService/**/*_baseline* +Kavita.Services.Tests/Test Data/ImageService/**/*.html +Kavita.Services.Tests/Test Data/ScannerService/ScanTests/**/* - -API.Tests/Services/Test Data/ScannerService/ScanTests/**/* +Kavita.Server/config/appsettings.*.json diff --git a/API.Benchmark/API.Benchmark.csproj b/API.Benchmark/API.Benchmark.csproj deleted file mode 100644 index e1451620e..000000000 --- a/API.Benchmark/API.Benchmark.csproj +++ /dev/null @@ -1,35 +0,0 @@ - - - - net10.0 - Exe - - - - - - - - - - - - - - - Always - - - - - Data - Always - - - - - PreserveNewest - - - - diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj deleted file mode 100644 index 098676370..000000000 --- a/API.Tests/API.Tests.csproj +++ /dev/null @@ -1,45 +0,0 @@ - - - - net10.0 - false - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - - - - - - - - - - PreserveNewest - - - - diff --git a/API.Tests/Comparers/ChapterSortComparerTest.cs b/API.Tests/Comparers/ChapterSortComparerTest.cs deleted file mode 100644 index 39a68b3b0..000000000 --- a/API.Tests/Comparers/ChapterSortComparerTest.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Linq; -using API.Comparators; -using Xunit; - -namespace API.Tests.Comparers; - -public class ChapterSortComparerDefaultLastTest -{ - [Theory] - [InlineData(new[] {1, 2, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber}, new[] {1, 2, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber})] - [InlineData(new[] {3, 1, 2}, new[] {1, 2, 3})] - [InlineData(new[] {1, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber}, new[] {1, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber})] - [InlineData(new[] {API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, 1}, new[] {1, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber})] - public void ChapterSortTest(int[] input, int[] expected) - { - Assert.Equal(expected, input.OrderBy(f => f, new ChapterSortComparerDefaultLast()).ToArray()); - } - -} diff --git a/API.Tests/Comparers/NumericComparerTests.cs b/API.Tests/Comparers/NumericComparerTests.cs deleted file mode 100644 index 8a1f23773..000000000 --- a/API.Tests/Comparers/NumericComparerTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using API.Comparators; -using Xunit; - -namespace API.Tests.Comparers; - -public class NumericComparerTests -{ - [Theory] - [InlineData( - new[] {"x1.jpg", "x10.jpg", "x3.jpg", "x4.jpg", "x11.jpg"}, - new[] {"x1.jpg", "x3.jpg", "x4.jpg", "x10.jpg", "x11.jpg"} - )] - [InlineData( - new[] {"x1.0.jpg", "0.5.jpg", "0.3.jpg"}, - new[] {"0.3.jpg", "0.5.jpg", "x1.0.jpg",} - )] - public void NumericComparerTest(string[] input, string[] expected) - { - var nc = new NumericComparer(); - Array.Sort(input, nc); - - var i = 0; - foreach (var s in input) - { - Assert.Equal(s, expected[i]); - i++; - } - } -} diff --git a/API.Tests/Comparers/SortComparerZeroLastTests.cs b/API.Tests/Comparers/SortComparerZeroLastTests.cs deleted file mode 100644 index 9a0722984..000000000 --- a/API.Tests/Comparers/SortComparerZeroLastTests.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Linq; -using API.Comparators; -using Xunit; - -namespace API.Tests.Comparers; - -public class SortComparerZeroLastTests -{ - [Theory] - [InlineData(new[] {API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, 1, 2,}, new[] {1, 2, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber})] - [InlineData(new[] {3, 1, 2}, new[] {1, 2, 3})] - [InlineData(new[] {API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, 1}, new[] {1, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber})] - public void SortComparerZeroLastTest(int[] input, int[] expected) - { - Assert.Equal(expected, input.OrderBy(f => f, ChapterSortComparerDefaultLast.Default).ToArray()); - } -} diff --git a/API.Tests/Comparers/StringLogicalComparerTest.cs b/API.Tests/Comparers/StringLogicalComparerTest.cs deleted file mode 100644 index 13f88243d..000000000 --- a/API.Tests/Comparers/StringLogicalComparerTest.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using API.Comparators; -using Xunit; - -namespace API.Tests.Comparers; - -public class StringLogicalComparerTest -{ - [Theory] - [InlineData( - new[] {"x1.jpg", "x10.jpg", "x3.jpg", "x4.jpg", "x11.jpg"}, - new[] {"x1.jpg", "x3.jpg", "x4.jpg", "x10.jpg", "x11.jpg"} - )] - [InlineData( - new[] {"a.jpg", "aaa.jpg", "1.jpg", }, - new[] {"1.jpg", "a.jpg", "aaa.jpg"} - )] - [InlineData( - new[] {"a.jpg", "aaa.jpg", "1.jpg", "!cover.png"}, - new[] {"!cover.png", "1.jpg", "a.jpg", "aaa.jpg"} - )] - public void StringComparer(string[] input, string[] expected) - { - Array.Sort(input, StringLogicalComparer.Compare); - - var i = 0; - foreach (var s in input) - { - Assert.Equal(s, expected[i]); - i++; - } - } -} diff --git a/API.Tests/Extensions/FileInfoExtensionsTests.cs b/API.Tests/Extensions/FileInfoExtensionsTests.cs deleted file mode 100644 index e708356a9..000000000 --- a/API.Tests/Extensions/FileInfoExtensionsTests.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using API.Extensions; -using Xunit; - -namespace API.Tests.Extensions; - -public class FileInfoExtensionsTests -{ - private static readonly string TestDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Extensions/Test Data/"); - - [Fact] - public void HasFileBeenModifiedSince_ShouldBeFalse() - { - var filepath = Path.Join(TestDirectory, "not modified.txt"); - var date = new FileInfo(filepath).LastWriteTime; - Assert.False(new FileInfo(filepath).HasFileBeenModifiedSince(date)); - File.ReadAllText(filepath); - Assert.False(new FileInfo(filepath).HasFileBeenModifiedSince(date)); - } - - [Fact] - public void HasFileBeenModifiedSince_ShouldBeTrue() - { - var filepath = Path.Join(TestDirectory, "modified on run.txt"); - var date = new FileInfo(filepath).LastWriteTime; - Assert.False(new FileInfo(filepath).HasFileBeenModifiedSince(date)); - File.AppendAllLines(filepath, new[] { DateTime.Now.ToString(CultureInfo.InvariantCulture) }); - Assert.True(new FileInfo(filepath).HasFileBeenModifiedSince(date)); - } -} diff --git a/API.Tests/Extensions/Test Data/not modified.txt b/API.Tests/Extensions/Test Data/not modified.txt deleted file mode 100644 index d5c0ce0a5..000000000 --- a/API.Tests/Extensions/Test Data/not modified.txt +++ /dev/null @@ -1 +0,0 @@ -Hello, this file should not be modified \ No newline at end of file diff --git a/API.Tests/Helpers/ParserInfoHelperTests.cs b/API.Tests/Helpers/ParserInfoHelperTests.cs deleted file mode 100644 index 0bb7efb9b..000000000 --- a/API.Tests/Helpers/ParserInfoHelperTests.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Collections.Generic; -using API.Entities.Enums; -using API.Helpers; -using API.Helpers.Builders; -using API.Services.Tasks.Scanner; -using API.Services.Tasks.Scanner.Parser; -using Xunit; - -namespace API.Tests.Helpers; - -public class ParserInfoHelperTests -{ - #region SeriesHasMatchingParserInfoFormat - - [Fact] - public void SeriesHasMatchingParserInfoFormat_ShouldBeFalse() - { - var infos = new Dictionary>(); - - ParserInfoFactory.AddToParsedInfo(infos, new ParserInfo() {Series = "Darker than Black", Volumes = "1", Format = MangaFormat.Archive}); - //AddToParsedInfo(infos, new ParserInfo() {Series = "Darker than Black", Volumes = "1", Format = MangaFormat.Epub}); - - var series = new SeriesBuilder("Darker Than Black") - .WithFormat(MangaFormat.Epub) - .WithVolume(new VolumeBuilder("1") - .WithName("1") - .Build()) - .WithLocalizedName("Darker Than Black") - .Build(); - - Assert.False(ParserInfoHelpers.SeriesHasMatchingParserInfoFormat(series, infos)); - } - - [Fact] - public void SeriesHasMatchingParserInfoFormat_ShouldBeTrue() - { - var infos = new Dictionary>(); - - ParserInfoFactory.AddToParsedInfo(infos, new ParserInfo() {Series = "Darker than Black", Volumes = "1", Format = MangaFormat.Archive}); - ParserInfoFactory.AddToParsedInfo(infos, new ParserInfo() {Series = "Darker than Black", Volumes = "1", Format = MangaFormat.Epub}); - - - var series = new SeriesBuilder("Darker Than Black") - .WithFormat(MangaFormat.Epub) - .WithVolume(new VolumeBuilder("1") - .WithName("1") - .Build()) - .WithLocalizedName("Darker Than Black") - .Build(); - - Assert.True(ParserInfoHelpers.SeriesHasMatchingParserInfoFormat(series, infos)); - } - - #endregion -} diff --git a/API.Tests/Helpers/ReviewHelperTests.cs b/API.Tests/Helpers/ReviewHelperTests.cs deleted file mode 100644 index 44e255390..000000000 --- a/API.Tests/Helpers/ReviewHelperTests.cs +++ /dev/null @@ -1,258 +0,0 @@ -using API.Helpers; -using System.Collections.Generic; -using System.Linq; -using Xunit; -using API.DTOs.SeriesDetail; - -namespace API.Tests.Helpers; - -public class ReviewHelperTests -{ - #region SelectSpectrumOfReviews Tests - - [Fact] - public void SelectSpectrumOfReviews_WhenLessThan10Reviews_ReturnsAllReviews() - { - - var reviews = CreateReviewList(8); - - // Act - var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); - - // Assert - Assert.Equal(8, result.Count); - Assert.Equal(reviews, result.OrderByDescending(r => r.Score)); - } - - [Fact] - public void SelectSpectrumOfReviews_WhenMoreThan10Reviews_Returns10Reviews() - { - - var reviews = CreateReviewList(20); - - // Act - var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); - - // Assert - Assert.Equal(10, result.Count); - Assert.Equal(reviews[0], result.First()); - Assert.Equal(reviews[19], result.Last()); - } - - [Fact] - public void SelectSpectrumOfReviews_WithExactly10Reviews_ReturnsAllReviews() - { - - var reviews = CreateReviewList(10); - - // Act - var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); - - // Assert - Assert.Equal(10, result.Count); - } - - [Fact] - public void SelectSpectrumOfReviews_WithLargeNumberOfReviews_ReturnsCorrectSpectrum() - { - - var reviews = CreateReviewList(100); - - // Act - var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); - - // Assert - Assert.Equal(10, result.Count); - Assert.Contains(reviews[0], result); - Assert.Contains(reviews[1], result); - Assert.Contains(reviews[98], result); - Assert.Contains(reviews[99], result); - } - - [Fact] - public void SelectSpectrumOfReviews_WithEmptyList_ReturnsEmptyList() - { - - var reviews = new List(); - - // Act - var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); - - // Assert - Assert.Empty(result); - } - - [Fact] - public void SelectSpectrumOfReviews_ResultsOrderedByScoreDescending() - { - - var reviews = new List - { - new UserReviewDto { Tagline = "1", Score = 3 }, - new UserReviewDto { Tagline = "2", Score = 5 }, - new UserReviewDto { Tagline = "3", Score = 1 }, - new UserReviewDto { Tagline = "4", Score = 4 }, - new UserReviewDto { Tagline = "5", Score = 2 } - }; - - // Act - var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); - - // Assert - Assert.Equal(5, result.Count); - Assert.Equal(5, result[0].Score); - Assert.Equal(4, result[1].Score); - Assert.Equal(3, result[2].Score); - Assert.Equal(2, result[3].Score); - Assert.Equal(1, result[4].Score); - } - - #endregion - - #region GetCharacters Tests - - [Fact] - public void GetCharacters_WithNullBody_ReturnsNull() - { - - string body = null; - - // Act - var result = ReviewHelper.GetCharacters(body); - - // Assert - Assert.Null(result); - } - - [Fact] - public void GetCharacters_WithEmptyBody_ReturnsEmptyString() - { - - var body = string.Empty; - - // Act - var result = ReviewHelper.GetCharacters(body); - - // Assert - Assert.Equal(string.Empty, result); - } - - [Fact] - public void GetCharacters_WithNoTextNodes_ReturnsEmptyString() - { - - const string body = "
"; - - // Act - var result = ReviewHelper.GetCharacters(body); - - // Assert - Assert.Equal(string.Empty, result); - } - - [Fact] - public void GetCharacters_WithLessCharactersThanLimit_ReturnsFullText() - { - - var body = "

This is a short review.

"; - - // Act - var result = ReviewHelper.GetCharacters(body); - - // Assert - Assert.Equal("This is a short review.…", result); - } - - [Fact] - public void GetCharacters_WithMoreCharactersThanLimit_TruncatesText() - { - - var body = "

" + new string('a', 200) + "

"; - - // Act - var result = ReviewHelper.GetCharacters(body); - - // Assert - Assert.Equal(new string('a', 175) + "…", result); - Assert.Equal(176, result.Length); // 175 characters + ellipsis - } - - [Fact] - public void GetCharacters_IgnoresScriptTags() - { - - const string body = "

Visible text

"; - - // Act - var result = ReviewHelper.GetCharacters(body); - - // Assert - Assert.Equal("Visible text…", result); - Assert.DoesNotContain("hidden", result); - } - - [Fact] - public void GetCharacters_RemovesMarkdownSymbols() - { - - const string body = "

This is **bold** and _italic_ text with [link](url).

"; - - // Act - var result = ReviewHelper.GetCharacters(body); - - // Assert - Assert.Equal("This is bold and italic text with link.…", result); - } - - [Fact] - public void GetCharacters_HandlesComplexMarkdownAndHtml() - { - - const string body = """ - -
-

# Header

-

This is ~~strikethrough~~ and __underlined__ text

-

~~~code block~~~

-

+++highlighted+++

-

img123(image.jpg)

-
- """; - - // Act - var result = ReviewHelper.GetCharacters(body); - - // Assert - Assert.DoesNotContain("~~", result); - Assert.DoesNotContain("__", result); - Assert.DoesNotContain("~~~", result); - Assert.DoesNotContain("+++", result); - Assert.DoesNotContain("img123(", result); - Assert.Contains("Header", result); - Assert.Contains("strikethrough", result); - Assert.Contains("underlined", result); - Assert.Contains("code block", result); - Assert.Contains("highlighted", result); - } - - #endregion - - #region Helper Methods - - private static List CreateReviewList(int count) - { - var reviews = new List(); - for (var i = 0; i < count; i++) - { - reviews.Add(new UserReviewDto - { - Tagline = $"{i + 1}", - Score = count - i // This makes them ordered by score descending initially - }); - } - return reviews; - } - - #endregion -} - diff --git a/API/API.csproj b/API/API.csproj deleted file mode 100644 index e5276ac0d..000000000 --- a/API/API.csproj +++ /dev/null @@ -1,216 +0,0 @@ - - - - Default - net10.0 - true - Linux - true - true - ../favicon.ico - warnings - latestmajor - false - - - - - false - ../favicon.ico - bin\$(Configuration)\$(AssemblyName).xml - - - - bin\$(Configuration)\$(AssemblyName).xml - 1701;1702;1591 - - - - - True - $(NoWarn);1591 - $(NoWarn);CA1873 - - - - en - - - - - Kavita - kareadita.github.io - Copyright 2020-$([System.DateTime]::Now.ToString('yyyy')) kavitareader.com (GNU General Public v3) - - $(Configuration)-dev - - false - false - false - - False - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Always - - - - - Always - - - - - - - - - Always - - - Always - - - - - - - <_DeploymentManifestIconFile Remove="favicon.ico" /> - - - diff --git a/API/API.csproj.DotSettings b/API/API.csproj.DotSettings deleted file mode 100644 index ced14c154..000000000 --- a/API/API.csproj.DotSettings +++ /dev/null @@ -1,4 +0,0 @@ - - True - True - True \ No newline at end of file diff --git a/API/Comparators/NumericComparer.cs b/API/Comparators/NumericComparer.cs deleted file mode 100644 index 17eeee059..000000000 --- a/API/Comparators/NumericComparer.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections; - -namespace API.Comparators; - -#nullable enable - -public class NumericComparer : IComparer -{ - - public int Compare(object? x, object? y) - { - if((x is string xs) && (y is string ys)) - { - return StringLogicalComparer.Compare(xs, ys); - } - return -1; - } -} diff --git a/API/Comparators/StringLogicalComparer.cs b/API/Comparators/StringLogicalComparer.cs deleted file mode 100644 index 6759454fb..000000000 --- a/API/Comparators/StringLogicalComparer.cs +++ /dev/null @@ -1,130 +0,0 @@ -//(c) Vasian Cepa 2005 -// Version 2 -// Taken from: https://www.codeproject.com/Articles/11016/Numeric-String-Sort-in-C - -using static System.Char; - -namespace API.Comparators; - - -public static class StringLogicalComparer -{ - public static int Compare(string s1, string s2) - { - //get rid of special cases - if((s1 == null) && (s2 == null)) return 0; - if(s1 == null) return -1; - if(s2 == null) return 1; - - if (string.IsNullOrEmpty(s1) && string.IsNullOrEmpty(s2)) return 0; - if (string.IsNullOrEmpty(s1)) return -1; - if (string.IsNullOrEmpty(s2)) return -1; - - //WE style, special case - var sp1 = IsLetterOrDigit(s1, 0); - var sp2 = IsLetterOrDigit(s2, 0); - if(sp1 && !sp2) return 1; - if(!sp1 && sp2) return -1; - - int i1 = 0, i2 = 0; //current index - while(true) - { - var c1 = IsDigit(s1, i1); - var c2 = IsDigit(s2, i2); - int r; // temp result - if(!c1 && !c2) - { - bool letter1 = IsLetter(s1, i1); - bool letter2 = IsLetter(s2, i2); - if((letter1 && letter2) || (!letter1 && !letter2)) - { - if(letter1 && letter2) - { - r = ToLower(s1[i1]).CompareTo(ToLower(s2[i2])); - } - else - { - r = s1[i1].CompareTo(s2[i2]); - } - if(r != 0) return r; - } - else if(!letter1 && letter2) return -1; - else if(letter1 && !letter2) return 1; - } - else if(c1 && c2) - { - r = CompareNum(s1, ref i1, s2, ref i2); - if(r != 0) return r; - } - else if(c1) - { - return -1; - } - else if(c2) - { - return 1; - } - i1++; - i2++; - if((i1 >= s1.Length) && (i2 >= s2.Length)) - { - return 0; - } - if(i1 >= s1.Length) - { - return -1; - } - if(i2 >= s2.Length) - { - return -1; - } - } - } - - private static int CompareNum(string s1, ref int i1, string s2, ref int i2) - { - int nzStart1 = i1, nzStart2 = i2; // nz = non zero - int end1 = i1, end2 = i2; - - ScanNumEnd(s1, i1, ref end1, ref nzStart1); - ScanNumEnd(s2, i2, ref end2, ref nzStart2); - var start1 = i1; i1 = end1 - 1; - var start2 = i2; i2 = end2 - 1; - - var nzLength1 = end1 - nzStart1; - var nzLength2 = end2 - nzStart2; - - if(nzLength1 < nzLength2) return -1; - if(nzLength1 > nzLength2) return 1; - - for(int j1 = nzStart1,j2 = nzStart2; j1 <= i1; j1++,j2++) - { - var r = s1[j1].CompareTo(s2[j2]); - if(r != 0) return r; - } - // the nz parts are equal - var length1 = end1 - start1; - var length2 = end2 - start2; - if(length1 == length2) return 0; - if(length1 > length2) return -1; - return 1; - } - - //lookahead - private static void ScanNumEnd(string s, int start, ref int end, ref int nzStart) - { - nzStart = start; - end = start; - var countZeros = true; - while(IsDigit(s, end)) - { - if(countZeros && s[end].Equals('0')) - { - nzStart++; - } - else countZeros = false; - end++; - if(end >= s.Length) break; - } - } -} diff --git a/API/Controllers/DeviceController.cs b/API/Controllers/DeviceController.cs deleted file mode 100644 index 869dd0b34..000000000 --- a/API/Controllers/DeviceController.cs +++ /dev/null @@ -1,250 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Device; -using API.DTOs.Device.ClientDevice; -using API.DTOs.Device.EmailDevice; -using API.DTOs.Progress; -using API.Services; -using API.SignalR; -using AutoMapper; -using Kavita.Common; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace API.Controllers; - -#nullable enable - -/// -/// Responsible interacting and creating Devices -/// -public class DeviceController : BaseApiController -{ - private readonly IUnitOfWork _unitOfWork; - private readonly IDeviceService _deviceService; - private readonly IEventHub _eventHub; - private readonly ILocalizationService _localizationService; - private readonly IMapper _mapper; - private readonly IClientDeviceService _clientDeviceService; - - public DeviceController(IUnitOfWork unitOfWork, IDeviceService deviceService,IEventHub eventHub, - ILocalizationService localizationService, IMapper mapper, IClientDeviceService clientDeviceService) - { - _unitOfWork = unitOfWork; - _deviceService = deviceService; - _eventHub = eventHub; - _localizationService = localizationService; - _mapper = mapper; - _clientDeviceService = clientDeviceService; - } - - - /// - /// Creates a new Device - /// - /// - /// - [HttpPost("create")] - public async Task> CreateOrUpdateDevice(CreateEmailDeviceDto dto) - { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Devices); - if (user == null) return Unauthorized(); - try - { - var device = await _deviceService.Create(dto, user); - if (device == null) - return BadRequest(await _localizationService.Translate(UserId, "generic-device-create")); - - return Ok(_mapper.Map(device)); - } - catch (KavitaException ex) - { - return BadRequest(await _localizationService.Translate(UserId, ex.Message)); - } - } - - /// - /// Updates an existing Device - /// - /// - /// - [HttpPost("update")] - public async Task> UpdateDevice(UpdateEmailDeviceDto dto) - { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Devices); - if (user == null) return Unauthorized(); - var device = await _deviceService.Update(dto, user); - - if (device == null) return BadRequest(await _localizationService.Translate(UserId, "generic-device-update")); - - return Ok(_mapper.Map(device)); - } - - /// - /// Deletes the device from the user - /// - /// - /// - [HttpDelete] - public async Task DeleteDevice(int deviceId) - { - if (deviceId <= 0) return BadRequest(await _localizationService.Translate(UserId, "device-doesnt-exist")); - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Devices); - if (user == null) return Unauthorized(); - if (await _deviceService.Delete(user, deviceId)) return Ok(); - - return BadRequest(await _localizationService.Translate(UserId, "generic-device-delete")); - } - - [HttpGet] - public async Task>> GetDevices() - { - return Ok(await _unitOfWork.DeviceRepository.GetDevicesForUserAsync(UserId)); - } - - /// - /// Sends a collection of chapters to the user's device - /// - /// - /// - [HttpPost("send-to")] - public async Task SendToDevice(SendToEmailDeviceDto dto) - { - var userId = UserId; - if (dto.ChapterIds.Any(i => i < 0)) return BadRequest(await _localizationService.Translate(userId, "greater-0", "ChapterIds")); - if (dto.DeviceId < 0) return BadRequest(await _localizationService.Translate(userId, "greater-0", "DeviceId")); - - var isEmailSetup = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).IsEmailSetupForSendToDevice(); - if (!isEmailSetup) - return BadRequest(await _localizationService.Translate(userId, "send-to-kavita-email")); - - // // Validate that the device belongs to the user - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.Devices); - if (user == null || user.Devices.All(d => d.Id != dto.DeviceId)) return BadRequest(await _localizationService.Translate(userId, "send-to-unallowed")); - - await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, - MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(userId, "send-to-device-status"), - "started"), userId); - try - { - var success = await _deviceService.SendTo(dto.ChapterIds, dto.DeviceId); - if (success) return Ok(); - } - catch (KavitaException ex) - { - return BadRequest(await _localizationService.Translate(userId, ex.Message)); - } - finally - { - await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, - MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(userId, "send-to-device-status"), - "ended"), userId); - } - - return BadRequest(await _localizationService.Translate(userId, "generic-send-to")); - } - - - /// - /// Attempts to send a whole series to a device. - /// - /// - /// - [HttpPost("send-series-to")] - public async Task SendSeriesToDevice(SendSeriesToEmailDeviceDto dto) - { - var userId = UserId; - if (dto.SeriesId <= 0) return BadRequest(await _localizationService.Translate(userId, "greater-0", "SeriesId")); - if (dto.DeviceId < 0) return BadRequest(await _localizationService.Translate(userId, "greater-0", "DeviceId")); - - var isEmailSetup = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).IsEmailSetupForSendToDevice(); - if (!isEmailSetup) - return BadRequest(await _localizationService.Translate(userId, "send-to-kavita-email")); - - await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, - MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(userId, "send-to-device-status"), - "started"), userId); - - var series = - await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, - SeriesIncludes.Volumes | SeriesIncludes.Chapters); - if (series == null) return BadRequest(await _localizationService.Translate(userId, "series-doesnt-exist")); - var chapterIds = series.Volumes.SelectMany(v => v.Chapters.Select(c => c.Id)).ToList(); - try - { - var success = await _deviceService.SendTo(chapterIds, dto.DeviceId); - if (success) return Ok(); - } - catch (KavitaException ex) - { - return BadRequest(await _localizationService.Translate(userId, ex.Message)); - } - finally - { - await _eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, - MessageFactory.SendingToDeviceEvent(await _localizationService.Translate(userId, "send-to-device-status"), - "ended"), userId); - } - - return BadRequest(await _localizationService.Translate(userId, "generic-send-to")); - } - - #region Client Devices - /// - /// Get my client devices - /// - /// - /// - [HttpGet("client/devices")] - public async Task>> GetMyClientDevices(bool includeInactive = false) - { - return Ok(await _clientDeviceService.GetUserDeviceDtosAsync(UserId, includeInactive)); - } - - /// - /// Get All user client devices - /// - /// - /// - [HttpGet("client/all-devices")] - [Authorize(PolicyGroups.AdminPolicy)] - public async Task>> GetAllClientDevices(bool includeInactive = false) - { - return Ok(await _clientDeviceService.GetAllUserDeviceDtos(includeInactive)); - } - - - /// - /// Removes the client device from DB - /// - /// - /// - [HttpDelete("client/device")] - public async Task> DeleteClientDevice(int clientDeviceId) - { - return Ok(await _clientDeviceService.DeleteDeviceAsync(UserId, clientDeviceId)); - } - - /// - /// Update the friendly name of the Device - /// - /// - /// - [HttpPost("client/update-name")] - public async Task UpdateClientDeviceName(UpdateClientDeviceNameDto dto) - { - await _clientDeviceService.UpdateFriendlyNameAsync(UserId, dto); - return Ok(); - } - - - - #endregion Client Devices - -} - - diff --git a/API/Controllers/EmailController.cs b/API/Controllers/EmailController.cs deleted file mode 100644 index dd52805e9..000000000 --- a/API/Controllers/EmailController.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.Email; -using API.Helpers; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace API.Controllers; - -[Authorize(Policy = PolicyGroups.AdminPolicy)] -public class EmailController : BaseApiController -{ - private readonly IUnitOfWork _unitOfWork; - - public EmailController(IUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } - - [HttpGet("all")] - public async Task>> GetEmails() - { - return Ok(await _unitOfWork.EmailHistoryRepository.GetEmailDtos(UserParams.Default)); - } -} diff --git a/API/Controllers/FallbackController.cs b/API/Controllers/FallbackController.cs deleted file mode 100644 index bb751d1df..000000000 --- a/API/Controllers/FallbackController.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.IO; -using API.Middleware; -using API.Services; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace API.Controllers; - -#nullable enable - -[AllowAnonymous] -public class FallbackController : Controller -{ - // ReSharper disable once S4487 - // ReSharper disable once NotAccessedField.Local -#pragma warning disable S4487 - private readonly ITaskScheduler _taskScheduler; -#pragma warning restore S4487 - - public FallbackController(ITaskScheduler taskScheduler) - { - // This is used to load TaskScheduler on startup without having to navigate to a Controller that uses. - _taskScheduler = taskScheduler; // TODO: Validate if this is needed as a DI anymore since we have a HostedStartupService - } - - [SkipDeviceTracking] - public IActionResult Index() - { - if (HttpContext.Request.Path.StartsWithSegments("/api")) - { - return NotFound(); - } - - return PhysicalFile(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "index.html"), "text/HTML"); - } -} - diff --git a/API/Controllers/ImageController.cs b/API/Controllers/ImageController.cs deleted file mode 100644 index ec76421a6..000000000 --- a/API/Controllers/ImageController.cs +++ /dev/null @@ -1,288 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Entities.Enums; -using API.Extensions; -using API.Middleware; -using API.Services; -using API.Services.Tasks.Metadata; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using MimeTypes; - -namespace API.Controllers; - -#nullable enable - -/// -/// Responsible for servicing up images stored in Kavita for entities -/// -[AllowAnonymous] -[SkipDeviceTracking] -public class ImageController : BaseApiController -{ - private readonly IUnitOfWork _unitOfWork; - private readonly IDirectoryService _directoryService; - private readonly ILocalizationService _localizationService; - private readonly IReadingListService _readingListService; - private readonly ICoverDbService _coverDbService; - - /// - public ImageController(IUnitOfWork unitOfWork, IDirectoryService directoryService, - ILocalizationService localizationService, IReadingListService readingListService, - ICoverDbService coverDbService) - { - _unitOfWork = unitOfWork; - _directoryService = directoryService; - _localizationService = localizationService; - _readingListService = readingListService; - _coverDbService = coverDbService; - } - - /// - /// Returns cover image for Chapter - /// - /// - /// - /// - [HttpGet("chapter-cover")] - public async Task GetChapterCoverImage(int chapterId, string apiKey) - { - var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ChapterRepository.GetChapterCoverImageAsync(chapterId)); - return PhysicalFile(path); - } - - /// - /// Returns cover image for Library - /// - /// - /// - /// - [HttpGet("library-cover")] - public async Task GetLibraryCoverImage(int libraryId, string apiKey) - { - var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.LibraryRepository.GetLibraryCoverImageAsync(libraryId)); - return PhysicalFile(path); - } - - /// - /// Returns cover image for Volume - /// - /// - /// - /// - [HttpGet("volume-cover")] - public async Task GetVolumeCoverImage(int volumeId, string apiKey) - { - var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.VolumeRepository.GetVolumeCoverImageAsync(volumeId)); - return PhysicalFile(path); - } - - /// - /// Returns cover image for Series - /// - /// Id of Series - /// - /// - [HttpGet("series-cover")] - public async Task GetSeriesCoverImage(int seriesId, string apiKey) - { - var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.SeriesRepository.GetSeriesCoverImageAsync(seriesId)); - return PhysicalFile(path); - } - - /// - /// Returns cover image for Collection - /// - /// - /// - /// - [HttpGet("collection-cover")] - public async Task GetCollectionCoverImage(int collectionTagId, string apiKey) - { - var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.CollectionTagRepository.GetCoverImageAsync(collectionTagId)); - if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) - { - // TODO: Streamline this like ReadingList does - path = await GenerateCollectionCoverImage(collectionTagId); - } - - return PhysicalFile(path); - } - - /// - /// Returns cover image for a Reading List - /// - /// - /// - /// - [HttpGet("readinglist-cover")] - public async Task GetReadingListCoverImage(int readingListId, string apiKey) - { - var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ReadingListRepository.GetCoverImageAsync(readingListId)); - if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) - { - path = await _readingListService.GenerateReadingListCoverImage(readingListId); - } - - return PhysicalFile(path); - } - - private async Task GenerateCollectionCoverImage(int collectionId) - { - var covers = await _unitOfWork.CollectionTagRepository.GetRandomCoverImagesAsync(collectionId); - var destFile = _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, - ImageService.GetCollectionTagFormat(collectionId)); - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - destFile += settings.EncodeMediaAs.GetExtension(); - - if (_directoryService.FileSystem.File.Exists(destFile)) return destFile; - ImageService.CreateMergedImage( - covers.Select(c => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, c)).ToList(), - settings.CoverImageSize, - destFile); - // TODO: Refactor this so that collections have a dedicated cover image so we can calculate primary/secondary colors - return !_directoryService.FileSystem.File.Exists(destFile) ? string.Empty : destFile; - } - - /// - /// Returns image for a given bookmark page - /// - /// This request is served unauthenticated, but user must be passed via api key to validate - /// - /// Starts at 0 - /// API Key for user. Needed to authenticate request - /// Only applicable for Epubs - handles multiple images on one page - /// - [HttpGet("bookmark")] - public async Task GetBookmarkImage(int chapterId, int pageNum, string apiKey, int imageOffset = 0) - { - var bookmark = await _unitOfWork.UserRepository.GetBookmarkForPage(pageNum, chapterId, imageOffset, UserId); - if (bookmark == null) return BadRequest(await _localizationService.Translate(UserId, "bookmark-doesnt-exist")); - - var bookmarkDirectory = - (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; - var path = Path.Join(bookmarkDirectory, bookmark.FileName); - - return PhysicalFile(path); - } - - /// - /// Returns the image associated with a web-link - /// - /// - /// - /// - [HttpGet("web-link")] - public async Task GetWebLinkImage(string url, string apiKey) - { - if (string.IsNullOrEmpty(url)) return BadRequest(await _localizationService.Translate(UserId, "must-be-defined", "Url")); - - var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; - - // Check if the domain exists - var domainFilePath = _directoryService.FileSystem.Path.Join(_directoryService.FaviconDirectory, ImageService.GetWebLinkFormat(url, encodeFormat)); - if (!_directoryService.FileSystem.File.Exists(domainFilePath)) - { - // We need to request the favicon and save it - try - { - domainFilePath = _directoryService.FileSystem.Path.Join(_directoryService.FaviconDirectory, - await _coverDbService.DownloadFaviconAsync(url, encodeFormat)); - } - catch (Exception) - { - return BadRequest(await _localizationService.Translate(UserId, "generic-favicon")); - } - } - - return PhysicalFile(domainFilePath); - } - - - /// - /// Returns the image associated with a publisher - /// - /// - /// - /// - [HttpGet("publisher")] - public async Task GetPublisherImage(string publisherName, string apiKey) - { - if (string.IsNullOrEmpty(publisherName)) return BadRequest(await _localizationService.Translate(UserId, "must-be-defined", "publisherName")); - if (publisherName.Contains("..")) return BadRequest(); - - var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; - - // Check if the domain exists - var domainFilePath = _directoryService.FileSystem.Path.Join(_directoryService.PublisherDirectory, ImageService.GetPublisherFormat(publisherName, encodeFormat)); - if (!_directoryService.FileSystem.File.Exists(domainFilePath)) - { - // We need to request the favicon and save it - try - { - domainFilePath = _directoryService.FileSystem.Path.Join(_directoryService.PublisherDirectory, - await _coverDbService.DownloadPublisherImageAsync(publisherName, encodeFormat)); - } - catch (Exception) - { - return BadRequest(await _localizationService.Translate(UserId, "generic-favicon")); - } - } - - return CachedFile(domainFilePath); - } - - /// - /// Returns cover image for Person - /// - /// - /// - /// - [HttpGet("person-cover")] - public async Task GetPersonCoverImage(int personId, string apiKey) - { - var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.UserRepository.GetPersonCoverImageAsync(personId)); - return PhysicalFile(path); - } - - /// - /// Returns cover image for User - /// - /// - /// - /// - [HttpGet("user-cover")] - public async Task GetUserCoverImage(int userId, string apiKey) - { - var filename = await _unitOfWork.UserRepository.GetCoverImageAsync(userId, UserId); - var path = Path.Join(_directoryService.CoverImageDirectory, filename); - return CachedFile(path); - } - - /// - /// Returns a temp coverupload image - /// - /// Requires Admin Role to perform upload - /// Filename of file. This is used with upload/upload-by-url - /// - /// - [HttpGet("cover-upload")] - public async Task GetCoverUploadImage(string filename, string apiKey) - { - if (!UserContext.IsAuthenticated) return Unauthorized(); - if (filename.Contains("..")) return BadRequest(await _localizationService.Translate(UserId, "invalid-filename")); - - var roles = await _unitOfWork.UserRepository.GetRolesByAuthKey(apiKey); - if (!roles.Contains(PolicyConstants.AdminRole)) - { - return Forbid(); - } - - var path = Path.Join(_directoryService.TempDirectory, filename); - return PhysicalFile(path); - } -} diff --git a/API/Controllers/VolumeController.cs b/API/Controllers/VolumeController.cs deleted file mode 100644 index 11d3d24ab..000000000 --- a/API/Controllers/VolumeController.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.Services; -using API.SignalR; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace API.Controllers; -#nullable enable - -public class VolumeController : BaseApiController -{ - private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; - private readonly IEventHub _eventHub; - - public VolumeController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IEventHub eventHub) - { - _unitOfWork = unitOfWork; - _localizationService = localizationService; - _eventHub = eventHub; - } - - /// - /// Returns the appropriate Volume - /// - /// - /// - [HttpGet] - public async Task> GetVolume(int volumeId) - { - return Ok(await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, UserId)); - } - - [Authorize(Policy = PolicyGroups.AdminPolicy)] - [HttpDelete] - public async Task> DeleteVolume(int volumeId) - { - var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(volumeId, - VolumeIncludes.Chapters | VolumeIncludes.People | VolumeIncludes.Tags); - if (volume == null) - return BadRequest(_localizationService.Translate(UserId, "volume-doesnt-exist")); - - _unitOfWork.VolumeRepository.Remove(volume); - - if (await _unitOfWork.CommitAsync()) - { - await _eventHub.SendMessageAsync(MessageFactory.VolumeRemoved, MessageFactory.VolumeRemovedEvent(volume.Id, volume.SeriesId), false); - return Ok(true); - } - - return Ok(false); - } - - [Authorize(Policy = PolicyGroups.AdminPolicy)] - [HttpPost("multiple")] - public async Task> DeleteMultipleVolumes(int[] volumesIds) - { - var volumes = await _unitOfWork.VolumeRepository.GetVolumesById(volumesIds); - if (volumes.Count != volumesIds.Length) - { - return BadRequest(_localizationService.Translate(UserId, "volume-doesnt-exist")); - } - - _unitOfWork.VolumeRepository.Remove(volumes); - - if (!await _unitOfWork.CommitAsync()) - { - return Ok(false); - } - - foreach (var volume in volumes) - { - await _eventHub.SendMessageAsync(MessageFactory.VolumeRemoved, MessageFactory.VolumeRemovedEvent(volume.Id, volume.SeriesId), false); - } - - return Ok(true); - } -} diff --git a/API/Data/Repositories/AppUserSmartFilterRepository.cs b/API/Data/Repositories/AppUserSmartFilterRepository.cs deleted file mode 100644 index 4c1adf784..000000000 --- a/API/Data/Repositories/AppUserSmartFilterRepository.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs.Dashboard; -using API.Entities; -using API.Helpers; -using AutoMapper; -using AutoMapper.QueryableExtensions; -using Microsoft.EntityFrameworkCore; - -namespace API.Data.Repositories; -#nullable enable - -public interface IAppUserSmartFilterRepository -{ - void Update(AppUserSmartFilter filter); - void Attach(AppUserSmartFilter filter); - void Delete(AppUserSmartFilter filter); - IEnumerable GetAllDtosByUserId(int userId); - Task> GetPagedDtosByUserIdAsync(int userId, UserParams userParams); - Task GetById(int smartFilterId); -} - -public class AppUserSmartFilterRepository : IAppUserSmartFilterRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public AppUserSmartFilterRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public void Update(AppUserSmartFilter filter) - { - _context.Entry(filter).State = EntityState.Modified; - } - - public void Attach(AppUserSmartFilter filter) - { - _context.AppUserSmartFilter.Attach(filter); - } - - public void Delete(AppUserSmartFilter filter) - { - _context.AppUserSmartFilter.Remove(filter); - } - - public IEnumerable GetAllDtosByUserId(int userId) - { - return _context.AppUserSmartFilter - .Where(f => f.AppUserId == userId) - .ProjectTo(_mapper.ConfigurationProvider) - .AsEnumerable(); - } - - public Task> GetPagedDtosByUserIdAsync(int userId, UserParams userParams) - { - var filters = _context.AppUserSmartFilter - .Where(f => f.AppUserId == userId) - .ProjectTo(_mapper.ConfigurationProvider); - - return PagedList.CreateAsync(filters, userParams); - } - - public async Task GetById(int smartFilterId) - { - return await _context.AppUserSmartFilter - .FirstOrDefaultAsync(d => d.Id == smartFilterId); - } -} diff --git a/API/Data/Repositories/CollectionTagRepository.cs b/API/Data/Repositories/CollectionTagRepository.cs deleted file mode 100644 index e1741c6c7..000000000 --- a/API/Data/Repositories/CollectionTagRepository.cs +++ /dev/null @@ -1,285 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Data.Misc; -using API.DTOs.Collection; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Extensions.QueryExtensions; -using API.Extensions.QueryExtensions.Filtering; -using API.Helpers; -using API.Services.Plus; -using AutoMapper; -using AutoMapper.QueryableExtensions; -using Microsoft.EntityFrameworkCore; - -namespace API.Data.Repositories; - -#nullable enable - -[Flags] -public enum CollectionTagIncludes -{ - None = 1, - SeriesMetadata = 2, - SeriesMetadataWithSeries = 4 -} - -[Flags] -public enum CollectionIncludes -{ - None = 1, - Series = 2, -} - -public interface ICollectionTagRepository -{ - void Remove(AppUserCollection tag); - Task GetCoverImageAsync(int collectionTagId); - Task GetCollectionAsync(int tagId, CollectionIncludes includes = CollectionIncludes.None); - void Update(AppUserCollection tag); - Task RemoveCollectionsWithoutSeries(); - - Task> GetAllCollectionsAsync(CollectionIncludes includes = CollectionIncludes.None); - /// - /// Returns all of the user's collections with the option of other user's promoted - /// - /// - /// - /// - Task> GetCollectionDtosAsync(int userId, bool includePromoted = false); - /// - /// Returns the collection if the user owns it or the collection is promoted - /// - /// - Task GetCollectionDtoAsync(int collectionId, int userId); - Task> GetCollectionDtosPagedAsync(int userId, UserParams userParams, bool includePromoted = false); - Task> GetCollectionDtosBySeriesAsync(int userId, int seriesId, bool includePromoted = false); - - Task> GetAllCoverImagesAsync(); - Task CollectionExists(string title, int userId); - Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat); - Task> GetRandomCoverImagesAsync(int collectionId); - Task> GetCollectionsForUserAsync(int userId, CollectionIncludes includes = CollectionIncludes.None); - Task UpdateCollectionAgeRating(AppUserCollection tag); - Task> GetCollectionsByIds(IEnumerable tags, CollectionIncludes includes = CollectionIncludes.None); - Task> GetAllCollectionsForSyncing(DateTime expirationTime); -} - -public class CollectionTagRepository : ICollectionTagRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public CollectionTagRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public void Remove(AppUserCollection tag) - { - _context.AppUserCollection.Remove(tag); - } - - public void Update(AppUserCollection tag) - { - _context.Entry(tag).State = EntityState.Modified; - } - - /// - /// Removes any collection tags without any series - /// - public async Task RemoveCollectionsWithoutSeries() - { - var tagsToDelete = await _context.AppUserCollection - .Include(c => c.Items) - .Where(c => c.Items.Count == 0) - .AsSplitQuery() - .ToListAsync(); - - _context.RemoveRange(tagsToDelete); - - return await _context.SaveChangesAsync(); - } - - public async Task> GetAllCollectionsAsync(CollectionIncludes includes = CollectionIncludes.None) - { - return await _context.AppUserCollection - .OrderBy(c => c.NormalizedTitle) - .Includes(includes) - .ToListAsync(); - } - - public async Task GetCollectionDtoAsync(int collectionId, int userId) - { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - return await _context.AppUserCollection - .Where(uc => (uc.AppUserId == userId || uc.Promoted) && uc.Id == collectionId) - .WhereIf(ageRating.AgeRating != AgeRating.NotApplicable, uc => uc.AgeRating <= ageRating.AgeRating) - .OrderBy(uc => uc.Title) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); - } - - public async Task> GetCollectionDtosAsync(int userId, bool includePromoted = false) - { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - return await _context.AppUserCollection - .Where(uc => uc.AppUserId == userId || (includePromoted && uc.Promoted)) - .WhereIf(ageRating.AgeRating != AgeRating.NotApplicable, uc => uc.AgeRating <= ageRating.AgeRating) - .OrderBy(uc => uc.Title) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } - - public async Task> GetCollectionDtosPagedAsync(int userId, UserParams userParams, bool includePromoted = false) - { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var collections = _context.AppUserCollection - .Where(uc => uc.AppUserId == userId || (includePromoted && uc.Promoted)) - .WhereIf(ageRating.AgeRating != AgeRating.NotApplicable, uc => uc.AgeRating <= ageRating.AgeRating) - .OrderBy(uc => uc.Title) - .ProjectTo(_mapper.ConfigurationProvider); - - return await PagedList.CreateAsync(collections, userParams); - } - - public async Task> GetCollectionDtosBySeriesAsync(int userId, int seriesId, bool includePromoted = false) - { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - return await _context.AppUserCollection - .Where(uc => uc.AppUserId == userId || (includePromoted && uc.Promoted)) - .Where(uc => uc.Items.Any(s => s.Id == seriesId)) - .WhereIf(ageRating.AgeRating != AgeRating.NotApplicable, uc => uc.AgeRating <= ageRating.AgeRating) - .OrderBy(uc => uc.Title) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } - - public async Task GetCoverImageAsync(int collectionTagId) - { - return await _context.AppUserCollection - .Where(c => c.Id == collectionTagId) - .Select(c => c.CoverImage) - .SingleOrDefaultAsync(); - } - - public async Task> GetAllCoverImagesAsync() - { - return await _context.AppUserCollection - .Select(t => t.CoverImage) - .Where(t => !string.IsNullOrEmpty(t)) - .ToListAsync(); - } - - /// - /// If any tag exists for that given user's collections - /// - /// - /// - /// - public async Task CollectionExists(string title, int userId) - { - var normalized = title.ToNormalized(); - return await _context.AppUserCollection - .Where(uc => uc.AppUserId == userId) - .AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized)); - } - - public async Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat) - { - var extension = encodeFormat.GetExtension(); - return await _context.AppUserCollection - .Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension)) - .ToListAsync(); - } - - public async Task> GetRandomCoverImagesAsync(int collectionId) - { - var random = new Random(); - var data = await _context.AppUserCollection - .Where(t => t.Id == collectionId) - .SelectMany(uc => uc.Items.Select(series => series.CoverImage)) - .Where(t => !string.IsNullOrEmpty(t)) - .ToListAsync(); - - return data - .OrderBy(_ => random.Next()) - .Take(4) - .ToList(); - } - - public async Task> GetCollectionsForUserAsync(int userId, CollectionIncludes includes = CollectionIncludes.None) - { - return await _context.AppUserCollection - .Where(c => c.AppUserId == userId) - .Includes(includes) - .ToListAsync(); - } - - public async Task UpdateCollectionAgeRating(AppUserCollection tag) - { - var maxAgeRating = await _context.AppUserCollection - .Where(t => t.Id == tag.Id) - .SelectMany(uc => uc.Items.Select(s => s.Metadata)) - .Select(sm => sm.AgeRating) - .ToListAsync(); - - - tag.AgeRating = maxAgeRating.Count != 0 ? maxAgeRating.Max() : AgeRating.Unknown; - await _context.SaveChangesAsync(); - } - - public async Task> GetCollectionsByIds(IEnumerable tags, CollectionIncludes includes = CollectionIncludes.None) - { - return await _context.AppUserCollection - .Where(c => tags.Contains(c.Id)) - .Includes(includes) - .AsSplitQuery() - .ToListAsync(); - } - - public async Task> GetAllCollectionsForSyncing(DateTime expirationTime) - { - return await _context.AppUserCollection - .Where(c => c.Source == ScrobbleProvider.Mal) - .Where(c => c.LastSyncUtc <= expirationTime) - .Include(c => c.Items) - .AsSplitQuery() - .ToListAsync(); - } - - public async Task GetCollectionAsync(int tagId, CollectionIncludes includes = CollectionIncludes.None) - { - return await _context.AppUserCollection - .Where(c => c.Id == tagId) - .Includes(includes) - .AsSplitQuery() - .SingleOrDefaultAsync(); - } - - private async Task GetUserAgeRestriction(int userId) - { - return await _context.AppUser - .AsNoTracking() - .Where(u => u.Id == userId) - .Select(u => - new AgeRestriction(){ - AgeRating = u.AgeRestriction, - IncludeUnknowns = u.AgeRestrictionIncludeUnknowns - }) - .SingleAsync(); - } - - public async Task> SearchTagDtosAsync(string searchQuery, int userId) - { - var userRating = await GetUserAgeRestriction(userId); - return await _context.AppUserCollection - .Search(searchQuery, userId, userRating) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } -} diff --git a/API/Data/Repositories/DeviceRepository.cs b/API/Data/Repositories/DeviceRepository.cs deleted file mode 100644 index 8dff1c93d..000000000 --- a/API/Data/Repositories/DeviceRepository.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs.Device; -using API.DTOs.Device.EmailDevice; -using API.Entities; -using AutoMapper; -using AutoMapper.QueryableExtensions; -using Microsoft.EntityFrameworkCore; - -namespace API.Data.Repositories; -#nullable enable - -public interface IDeviceRepository -{ - void Update(Device device); - Task> GetDevicesForUserAsync(int userId); - Task GetDeviceById(int deviceId); -} - -public class DeviceRepository : IDeviceRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public DeviceRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public void Update(Device device) - { - _context.Entry(device).State = EntityState.Modified; - } - - public async Task> GetDevicesForUserAsync(int userId) - { - return await _context.Device - .Where(d => d.AppUserId == userId) - .OrderBy(d => d.LastUsed) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } - - public async Task GetDeviceById(int deviceId) - { - return await _context.Device - .Where(d => d.Id == deviceId) - .SingleOrDefaultAsync(); - } -} diff --git a/API/Data/Repositories/EmailHistoryRepository.cs b/API/Data/Repositories/EmailHistoryRepository.cs deleted file mode 100644 index f6f49fa34..000000000 --- a/API/Data/Repositories/EmailHistoryRepository.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs.Email; -using API.Helpers; -using AutoMapper; -using AutoMapper.QueryableExtensions; -using Microsoft.EntityFrameworkCore; - -namespace API.Data.Repositories; - -public interface IEmailHistoryRepository -{ - Task> GetEmailDtos(UserParams userParams); -} - -public class EmailHistoryRepository : IEmailHistoryRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public EmailHistoryRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - - public async Task> GetEmailDtos(UserParams userParams) - { - return await _context.EmailHistory - .OrderByDescending(h => h.SendDate) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } -} diff --git a/API/Data/Repositories/EpubFontRepository.cs b/API/Data/Repositories/EpubFontRepository.cs deleted file mode 100644 index cea0d068a..000000000 --- a/API/Data/Repositories/EpubFontRepository.cs +++ /dev/null @@ -1,102 +0,0 @@ -#nullable enable -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs.Font; -using API.Entities; -using API.Extensions; -using API.Services.Tasks; -using AutoMapper; -using AutoMapper.QueryableExtensions; -using Microsoft.EntityFrameworkCore; - -namespace API.Data.Repositories; - -public interface IEpubFontRepository -{ - void Add(EpubFont font); - void Remove(EpubFont font); - void Update(EpubFont font); - Task> GetFontDtosAsync(); - Task GetFontDtoAsync(int fontId); - Task GetFontDtoByNameAsync(string name); - Task> GetFontsAsync(); - Task GetFontAsync(int fontId); - Task IsFontInUseAsync(int fontId); -} - -public class EpubFontRepository: IEpubFontRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public EpubFontRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public void Add(EpubFont font) - { - _context.Add(font); - } - - public void Remove(EpubFont font) - { - _context.Remove(font); - } - - public void Update(EpubFont font) - { - _context.Entry(font).State = EntityState.Modified; - } - - public async Task> GetFontDtosAsync() - { - return await _context.EpubFont - .OrderBy(s => s.Name == FontService.DefaultFont ? -1 : 0) - .ThenBy(s => s) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } - - public async Task GetFontDtoAsync(int fontId) - { - return await _context.EpubFont - .Where(f => f.Id == fontId) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); - } - - public async Task GetFontDtoByNameAsync(string name) - { - return await _context.EpubFont - .Where(f => f.NormalizedName.Equals(name.ToNormalized())) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); - } - - public async Task> GetFontsAsync() - { - return await _context.EpubFont - .ToListAsync(); - } - - public async Task GetFontAsync(int fontId) - { - return await _context.EpubFont - .Where(f => f.Id == fontId) - .FirstOrDefaultAsync(); - } - - public async Task IsFontInUseAsync(int fontId) - { - return await _context.AppUserReadingProfiles - .Join(_context.EpubFont, - preference => preference.BookReaderFontFamily, - font => font.Name, - (preference, font) => new { preference, font }) - .AnyAsync(joined => joined.font.Id == fontId); - } - -} diff --git a/API/Data/Repositories/MangaFileRepository.cs b/API/Data/Repositories/MangaFileRepository.cs deleted file mode 100644 index 89c6bb418..000000000 --- a/API/Data/Repositories/MangaFileRepository.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Entities; -using Microsoft.EntityFrameworkCore; - -namespace API.Data.Repositories; -#nullable enable - -public interface IMangaFileRepository -{ - void Update(MangaFile file); - Task> GetAllWithMissingExtension(); - Task GetByKoreaderHash(string hash); -} - -public class MangaFileRepository : IMangaFileRepository -{ - private readonly DataContext _context; - - public MangaFileRepository(DataContext context) - { - _context = context; - } - - public void Update(MangaFile file) - { - _context.Entry(file).State = EntityState.Modified; - } - - public async Task> GetAllWithMissingExtension() - { - return await _context.MangaFile - .Where(f => string.IsNullOrEmpty(f.Extension)) - .ToListAsync(); - } - - public async Task GetByKoreaderHash(string hash) - { - if (string.IsNullOrEmpty(hash)) return null; - - return await _context.MangaFile - .FirstOrDefaultAsync(f => f.KoreaderHash != null && - f.KoreaderHash.Equals(hash.ToUpper())); - } -} diff --git a/API/Data/Repositories/MediaErrorRepository.cs b/API/Data/Repositories/MediaErrorRepository.cs deleted file mode 100644 index eac2ee295..000000000 --- a/API/Data/Repositories/MediaErrorRepository.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs.MediaErrors; -using API.Entities; -using AutoMapper; -using AutoMapper.QueryableExtensions; -using Microsoft.EntityFrameworkCore; - -namespace API.Data.Repositories; -#nullable enable - -public interface IMediaErrorRepository -{ - void Attach(MediaError error); - void Remove(MediaError error); - void Remove(IList errors); - Task Find(string filename); - IEnumerable GetAllErrorDtosAsync(); - Task ExistsAsync(MediaError error); - Task DeleteAll(); - Task> GetAllErrorsAsync(IList comments); -} - -public class MediaErrorRepository : IMediaErrorRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public MediaErrorRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public void Attach(MediaError? error) - { - if (error == null) return; - _context.MediaError.Attach(error); - } - - public void Remove(MediaError? error) - { - if (error == null) return; - _context.MediaError.Remove(error); - } - - public void Remove(IList errors) - { - _context.MediaError.RemoveRange(errors); - } - - public Task Find(string filename) - { - return _context.MediaError.Where(e => e.FilePath == filename).SingleOrDefaultAsync(); - } - - public IEnumerable GetAllErrorDtosAsync() - { - var query = _context.MediaError - .OrderByDescending(m => m.Created) - .ProjectTo(_mapper.ConfigurationProvider) - .AsNoTracking(); - return query.AsEnumerable(); - } - - public Task ExistsAsync(MediaError error) - { - return _context.MediaError.AnyAsync(m => m.FilePath.Equals(error.FilePath) - && m.Comment.Equals(error.Comment) - && m.Details.Equals(error.Details) - ); - } - - public async Task DeleteAll() - { - _context.MediaError.RemoveRange(await _context.MediaError.ToListAsync()); - await _context.SaveChangesAsync(); - } - - public Task> GetAllErrorsAsync(IList comments) - { - return _context.MediaError - .Where(m => comments.Contains(m.Comment)) - .ToListAsync(); - } -} diff --git a/API/Data/Repositories/ScrobbleEventRepository.cs b/API/Data/Repositories/ScrobbleEventRepository.cs deleted file mode 100644 index 8a484f697..000000000 --- a/API/Data/Repositories/ScrobbleEventRepository.cs +++ /dev/null @@ -1,225 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs.Scrobbling; -using API.Entities.Scrobble; -using API.Extensions.QueryExtensions; -using API.Helpers; -using AutoMapper; -using AutoMapper.QueryableExtensions; -using Microsoft.EntityFrameworkCore; - -namespace API.Data.Repositories; -#nullable enable - -public interface IScrobbleRepository -{ - void Attach(ScrobbleEvent evt); - void Attach(ScrobbleError error); - void Remove(ScrobbleEvent evt); - void Remove(IEnumerable events); - void Remove(IEnumerable errors); - void Update(ScrobbleEvent evt); - Task> GetByEvent(ScrobbleEventType type, bool isProcessed = false); - Task> GetProcessedEvents(int daysAgo); - Task Exists(int userId, int seriesId, ScrobbleEventType eventType); - Task> GetScrobbleErrors(); - Task> GetAllScrobbleErrorsForSeries(int seriesId); - Task ClearScrobbleErrors(); - Task HasErrorForSeries(int seriesId); - /// - /// Get all events for a specific user and type - /// - /// - /// - /// - /// If true, only returned not processed events - /// - Task GetEvent(int userId, int seriesId, ScrobbleEventType eventType, bool isNotProcessed = false); - Task> GetUserEventsForSeries(int userId, int seriesId); - /// - /// Return the events with given ids, when belonging to the passed user - /// - /// - /// - /// - Task> GetUserEvents(int userId, IList scrobbleEventIds); - Task> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination); - Task> GetAllEventsForSeries(int seriesId); - Task> GetAllEventsWithSeriesIds(IEnumerable seriesIds); - Task> GetEvents(); -} - -/// -/// This handles everything around Scrobbling -/// -public class ScrobbleRepository : IScrobbleRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public ScrobbleRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public void Attach(ScrobbleEvent evt) - { - _context.ScrobbleEvent.Attach(evt); - } - - public void Attach(ScrobbleError error) - { - _context.ScrobbleError.Attach(error); - } - - public void Remove(ScrobbleEvent evt) - { - _context.ScrobbleEvent.Remove(evt); - } - - public void Remove(IEnumerable events) - { - _context.ScrobbleEvent.RemoveRange(events); - } - - public void Remove(IEnumerable errors) - { - _context.ScrobbleError.RemoveRange(errors); - } - - public void Update(ScrobbleEvent evt) - { - _context.Entry(evt).State = EntityState.Modified; - } - - public async Task> GetByEvent(ScrobbleEventType type, bool isProcessed = false) - { - return await _context.ScrobbleEvent - .Include(s => s.Series) - .ThenInclude(s => s.Library) - .Include(s => s.Series) - .ThenInclude(s => s.Metadata) - .Include(s => s.AppUser) - .ThenInclude(u => u.UserPreferences) - .Where(s => s.ScrobbleEventType == type) - .Where(s => s.IsProcessed == isProcessed) - .AsSplitQuery() - .GroupBy(s => s.SeriesId) - .Select(g => g.OrderByDescending(e => e.ChapterNumber) - .ThenByDescending(e => e.VolumeNumber) - .FirstOrDefault()) - .ToListAsync(); - } - - /// - /// Returns all processed events that were processed 7 or more days ago - /// - /// - /// - public async Task> GetProcessedEvents(int daysAgo) - { - var date = DateTime.UtcNow.Subtract(TimeSpan.FromDays(daysAgo)); - return await _context.ScrobbleEvent - .Where(s => s.IsProcessed) - .Where(s => s.ProcessDateUtc != null && s.ProcessDateUtc < date) - .ToListAsync(); - } - - public async Task Exists(int userId, int seriesId, ScrobbleEventType eventType) - { - return await _context.ScrobbleEvent.AnyAsync(e => - e.AppUserId == userId && e.SeriesId == seriesId && e.ScrobbleEventType == eventType); - } - - public async Task> GetScrobbleErrors() - { - return await _context.ScrobbleError - .OrderBy(e => e.LastModifiedUtc) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } - - public async Task> GetAllScrobbleErrorsForSeries(int seriesId) - { - return await _context.ScrobbleError - .Where(e => e.SeriesId == seriesId) - .ToListAsync(); - } - - public async Task ClearScrobbleErrors() - { - _context.ScrobbleError.RemoveRange(_context.ScrobbleError); - await _context.SaveChangesAsync(); - } - - public async Task HasErrorForSeries(int seriesId) - { - return await _context.ScrobbleError.AnyAsync(n => n.SeriesId == seriesId); - } - - public async Task GetEvent(int userId, int seriesId, ScrobbleEventType eventType, bool isNotProcessed = false) - { - return await _context.ScrobbleEvent - .Where(e => e.AppUserId == userId && e.SeriesId == seriesId && e.ScrobbleEventType == eventType) - .WhereIf(isNotProcessed, e => !e.IsProcessed) - .OrderBy(e => e.LastModifiedUtc) - .FirstOrDefaultAsync(); - } - - public async Task> GetUserEventsForSeries(int userId, int seriesId) - { - return await _context.ScrobbleEvent - .Where(e => e.AppUserId == userId && !e.IsProcessed && e.SeriesId == seriesId) - .Include(e => e.Series) - .OrderBy(e => e.LastModifiedUtc) - .AsSplitQuery() - .ToListAsync(); - } - - public async Task> GetUserEvents(int userId, IList scrobbleEventIds) - { - return await _context.ScrobbleEvent - .Where(e => e.AppUserId == userId && scrobbleEventIds.Contains(e.Id)) - .ToListAsync(); - } - - public async Task> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination) - { - var query = _context.ScrobbleEvent - .Where(e => e.AppUserId == userId) - .Include(e => e.Series) - .WhereIf(!string.IsNullOrEmpty(filter.Query), s => - EF.Functions.Like(s.Series.Name, $"%{filter.Query}%") - ) - .WhereIf(!filter.IncludeReviews, e => e.ScrobbleEventType != ScrobbleEventType.Review) - .SortBy(filter.Field, filter.IsDescending) - .AsSplitQuery() - .ProjectTo(_mapper.ConfigurationProvider); - - return await PagedList.CreateAsync(query, pagination.PageNumber, pagination.PageSize); - } - - public async Task> GetAllEventsForSeries(int seriesId) - { - return await _context.ScrobbleEvent - .Where(e => e.SeriesId == seriesId) - .ToListAsync(); - } - - public async Task> GetAllEventsWithSeriesIds(IEnumerable seriesIds) - { - return await _context.ScrobbleEvent - .Where(e => seriesIds.Contains(e.SeriesId)) - .ToListAsync(); - } - - public async Task> GetEvents() - { - return await _context.ScrobbleEvent - .Include(e => e.AppUser) - .ToListAsync(); - } -} diff --git a/API/Data/Repositories/SeriesMetadataRepository.cs b/API/Data/Repositories/SeriesMetadataRepository.cs deleted file mode 100644 index 0a3efee26..000000000 --- a/API/Data/Repositories/SeriesMetadataRepository.cs +++ /dev/null @@ -1,23 +0,0 @@ -using API.Entities.Metadata; - -namespace API.Data.Repositories; - -public interface ISeriesMetadataRepository -{ - void Update(SeriesMetadata seriesMetadata); -} - -public class SeriesMetadataRepository : ISeriesMetadataRepository -{ - private readonly DataContext _context; - - public SeriesMetadataRepository(DataContext context) - { - _context = context; - } - - public void Update(SeriesMetadata seriesMetadata) - { - _context.SeriesMetadata.Update(seriesMetadata); - } -} diff --git a/API/Data/Repositories/SettingsRepository.cs b/API/Data/Repositories/SettingsRepository.cs deleted file mode 100644 index 2e9eb4262..000000000 --- a/API/Data/Repositories/SettingsRepository.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Settings; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Entities.MetadataMatching; -using AutoMapper; -using AutoMapper.QueryableExtensions; -using Microsoft.EntityFrameworkCore; - -namespace API.Data.Repositories; -#nullable enable - -public interface ISettingsRepository -{ - void Update(ServerSetting settings); - void Update(MetadataSettings settings); - void RemoveRange(List fieldMappings); - Task GetSettingsDtoAsync(); - Task GetSettingAsync(ServerSettingKey key); - Task> GetSettingsAsync(); - void Remove(ServerSetting setting); - Task GetExternalSeriesMetadata(int seriesId); - Task GetMetadataSettings(); - Task GetMetadataSettingDto(); -} -public class SettingsRepository : ISettingsRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public SettingsRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public void Update(ServerSetting settings) - { - _context.Entry(settings).State = EntityState.Modified; - } - - public void Update(MetadataSettings settings) - { - _context.Entry(settings).State = EntityState.Modified; - } - - public void RemoveRange(List fieldMappings) - { - _context.MetadataFieldMapping.RemoveRange(fieldMappings); - } - - public void Remove(ServerSetting setting) - { - _context.Remove(setting); - } - - public async Task GetExternalSeriesMetadata(int seriesId) - { - return await _context.ExternalSeriesMetadata - .Where(s => s.SeriesId == seriesId) - .FirstOrDefaultAsync(); - } - - public async Task GetMetadataSettings() - { - return await _context.MetadataSettings - .Include(m => m.FieldMappings) - .FirstAsync(); - } - - public async Task GetMetadataSettingDto() - { - return await _context.MetadataSettings - .Include(m => m.FieldMappings) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstAsync(); - } - - public async Task GetSettingsDtoAsync() - { - var settings = await _context.ServerSetting - .Select(x => x) - .AsNoTracking() - .ToListAsync(); - return _mapper.Map(settings); - } - - public Task GetSettingAsync(ServerSettingKey key) - { - return _context.ServerSetting.SingleOrDefaultAsync(x => x.Key == key)!; - } - - public async Task> GetSettingsAsync() - { - return await _context.ServerSetting.ToListAsync(); - } -} diff --git a/API/Data/Repositories/SiteThemeRepository.cs b/API/Data/Repositories/SiteThemeRepository.cs deleted file mode 100644 index 33517e846..000000000 --- a/API/Data/Repositories/SiteThemeRepository.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs.Theme; -using API.Entities; -using AutoMapper; -using AutoMapper.QueryableExtensions; -using Microsoft.EntityFrameworkCore; - -namespace API.Data.Repositories; -#nullable enable - -public interface ISiteThemeRepository -{ - void Add(SiteTheme theme); - void Remove(SiteTheme theme); - void Update(SiteTheme siteTheme); - Task> GetThemeDtos(); - Task GetThemeDto(int themeId); - Task GetThemeDtoByName(string themeName); - Task GetDefaultTheme(); - Task> GetThemes(); - Task GetTheme(int themeId); - Task IsThemeInUse(int themeId); -} - -public class SiteThemeRepository : ISiteThemeRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public SiteThemeRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public void Add(SiteTheme theme) - { - _context.Add(theme); - } - - public void Remove(SiteTheme theme) - { - _context.Remove(theme); - } - - public void Update(SiteTheme siteTheme) - { - _context.Entry(siteTheme).State = EntityState.Modified; - } - - public async Task> GetThemeDtos() - { - return await _context.SiteTheme - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); - } - - public async Task GetThemeDtoByName(string themeName) - { - return await _context.SiteTheme - .Where(t => t.Name.Equals(themeName)) - .ProjectTo(_mapper.ConfigurationProvider) - .SingleOrDefaultAsync(); - } - - /// - /// Returns default theme, if the default theme is not available, returns the dark theme - /// - /// - public async Task GetDefaultTheme() - { - var result = await _context.SiteTheme - .Where(t => t.IsDefault) - .FirstOrDefaultAsync(); - - if (result == null) - { - return await _context.SiteTheme - .Where(t => t.NormalizedName == Seed.DefaultThemes[0].NormalizedName) - .SingleAsync(); - } - - return result; - } - - public async Task> GetThemes() - { - return await _context.SiteTheme - .ToListAsync(); - } - - public async Task GetTheme(int themeId) - { - return await _context.SiteTheme - .Where(t => t.Id == themeId) - .FirstOrDefaultAsync(); - } - - public async Task IsThemeInUse(int themeId) - { - return await _context.AppUserPreferences - .AnyAsync(p => p.Theme.Id == themeId); - } - - public async Task GetThemeDto(int themeId) - { - return await _context.SiteTheme - .Where(t => t.Id == themeId) - .ProjectTo(_mapper.ConfigurationProvider) - .SingleOrDefaultAsync(); - } -} diff --git a/API/Data/Repositories/UserTableOfContentRepository.cs b/API/Data/Repositories/UserTableOfContentRepository.cs deleted file mode 100644 index 34b3994de..000000000 --- a/API/Data/Repositories/UserTableOfContentRepository.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs.Reader; -using API.Entities; -using AutoMapper; -using AutoMapper.QueryableExtensions; -using Microsoft.EntityFrameworkCore; - -namespace API.Data.Repositories; -#nullable enable - -public interface IUserTableOfContentRepository -{ - void Attach(AppUserTableOfContent toc); - void Remove(AppUserTableOfContent toc); - Task IsUnique(int userId, int chapterId, int page, string title); - IEnumerable GetPersonalToC(int userId, int chapterId); - Task> GetPersonalToCForPage(int userId, int chapterId, int page); - Task Get(int userId, int chapterId, int pageNum, string title); -} - -public class UserTableOfContentRepository : IUserTableOfContentRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public UserTableOfContentRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public void Attach(AppUserTableOfContent toc) - { - _context.AppUserTableOfContent.Attach(toc); - } - - public void Remove(AppUserTableOfContent toc) - { - _context.AppUserTableOfContent.Remove(toc); - } - - public async Task IsUnique(int userId, int chapterId, int page, string title) - { - return await _context.AppUserTableOfContent.AnyAsync(t => - t.AppUserId == userId && t.PageNumber == page && t.Title == title && t.ChapterId == chapterId); - } - - public IEnumerable GetPersonalToC(int userId, int chapterId) - { - return _context.AppUserTableOfContent - .Where(t => t.AppUserId == userId && t.ChapterId == chapterId) - .ProjectTo(_mapper.ConfigurationProvider) - .OrderBy(t => t.PageNumber) - .AsEnumerable(); - } - - public async Task> GetPersonalToCForPage(int userId, int chapterId, int page) - { - return await _context.AppUserTableOfContent - .Where(t => t.AppUserId == userId && t.ChapterId == chapterId && t.PageNumber == page) - .ProjectTo(_mapper.ConfigurationProvider) - .OrderBy(t => t.PageNumber) - .ToListAsync(); - } - - public async Task Get(int userId,int chapterId, int pageNum, string title) - { - return await _context.AppUserTableOfContent - .Where(t => t.AppUserId == userId && t.ChapterId == chapterId && t.PageNumber == pageNum && t.Title == title) - .FirstOrDefaultAsync(); - } -} diff --git a/API/Extensions/AppUserExtensions.cs b/API/Extensions/AppUserExtensions.cs deleted file mode 100644 index be3d2c064..000000000 --- a/API/Extensions/AppUserExtensions.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using API.Data.Misc; -using API.Entities; -using API.Helpers; - -namespace API.Extensions; -#nullable enable - -public static class AppUserExtensions -{ - /// - /// Adds a new SideNavStream to the user's SideNavStreams. This user should have these streams already loaded - /// - /// - /// - public static void CreateSideNavFromLibrary(this AppUser user, Library library) - { - user.SideNavStreams ??= new List(); - var maxCount = user.SideNavStreams.Select(s => s.Order).DefaultIfEmpty().Max(); - - if (user.SideNavStreams.FirstOrDefault(s => s.LibraryId == library.Id) != null) return; - - user.SideNavStreams.Add(new AppUserSideNavStream() - { - Name = library.Name, - Order = maxCount + 1, - IsProvided = false, - StreamType = SideNavStreamType.Library, - LibraryId = library.Id, - Visible = true, - }); - } - - - public static void RemoveSideNavFromLibrary(this AppUser user, Library library) - { - user.SideNavStreams ??= new List(); - - // Find the library and remove it - var item = user.SideNavStreams.FirstOrDefault(s => s.LibraryId == library.Id); - if (item == null) return; - user.SideNavStreams.Remove(item); - - OrderableHelper.ReorderItems(user.SideNavStreams); - - } - - public static AgeRestriction GetAgeRestriction(this AppUser user) - { - return new AgeRestriction() - { - AgeRating = user.AgeRestriction, - IncludeUnknowns = user.AgeRestrictionIncludeUnknowns, - }; - } -} diff --git a/API/Extensions/EnumExtensions.cs b/API/Extensions/EnumExtensions.cs deleted file mode 100644 index 63e28b8ab..000000000 --- a/API/Extensions/EnumExtensions.cs +++ /dev/null @@ -1,43 +0,0 @@ -#nullable enable -using System; -using System.ComponentModel; -using System.Reflection; - -namespace API.Extensions; - -public static class EnumExtensions -{ - /// - /// Extension on Enum.TryParse which also tried matching on the description attribute - /// - /// if a match was found - /// First tries Enum.TryParse then fall back to the more expensive operation - public static bool TryParse(string? value, out TEnum result) where TEnum : struct, Enum - { - result = default; - - if (string.IsNullOrEmpty(value)) - { - return false; - } - - if (Enum.TryParse(value, out result)) - { - return true; - } - - foreach (var field in typeof(TEnum).GetFields(BindingFlags.Public | BindingFlags.Static)) - { - var description = field.GetCustomAttribute()?.Description; - - if (!string.IsNullOrEmpty(description) && - string.Equals(description, value, StringComparison.OrdinalIgnoreCase)) - { - result = (TEnum)field.GetValue(null)!; - return true; - } - } - - return false; - } -} diff --git a/API/Extensions/EnumerableExtensions.cs b/API/Extensions/EnumerableExtensions.cs deleted file mode 100644 index 903b6f869..000000000 --- a/API/Extensions/EnumerableExtensions.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using API.Data.Misc; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; - -namespace API.Extensions; -#nullable enable - -public static class EnumerableExtensions -{ - private static readonly Regex Regex = new Regex(@"\d+", RegexOptions.Compiled, TimeSpan.FromMilliseconds(500)); - - /// - /// A natural sort implementation - /// - /// IEnumerable to process - /// Function that produces a string. Does not support null values - /// Defaults to CurrentCulture - /// - /// Sorted Enumerable - public static IEnumerable OrderByNatural(this IEnumerable items, Func selector, StringComparer? stringComparer = null) - { - var list = items.ToList(); - var maxDigits = list - .SelectMany(i => Regex.Matches(selector(i)) - .Select(digitChunk => (int?)digitChunk.Value.Length)) - .Max() ?? 0; - - return list.OrderBy(i => Regex.Replace(selector(i), match => match.Value.PadLeft(maxDigits, '0')), stringComparer ?? StringComparer.CurrentCulture); - } - - public static IEnumerable RestrictAgainstAgeRestriction(this IEnumerable items, AgeRestriction restriction) - { - if (restriction.AgeRating == AgeRating.NotApplicable) return items; - var q = items.Where(s => s.AgeRating <= restriction.AgeRating); - if (!restriction.IncludeUnknowns) - { - return q.Where(s => s.AgeRating != AgeRating.Unknown); - } - - return q; - } - - public static IEnumerable RestrictAgainstAgeRestriction(this IEnumerable items, AgeRestriction restriction) - { - if (restriction.AgeRating == AgeRating.NotApplicable) return items; - var q = items.Where(s => s.AgeRating <= restriction.AgeRating); - if (!restriction.IncludeUnknowns) - { - return q.Where(s => s.AgeRating != AgeRating.Unknown); - } - - return q; - } - - public static IEnumerable RestrictAgainstAgeRestriction(this IEnumerable items, AgeRestriction restriction) - { - if (restriction.AgeRating == AgeRating.NotApplicable) return items; - var q = items.Where(s => s.AgeRating <= restriction.AgeRating); - if (!restriction.IncludeUnknowns) - { - return q.Where(s => s.AgeRating != AgeRating.Unknown); - } - - return q; - } - - /// - /// Safety net around Max, returning the default value if source contains no elements - /// - /// - /// - /// - /// - /// - /// - public static TResult? MaxOrDefault( - this IList source, - Func selector, - TResult? defaultValue) - { - return source.Count == 0 ? defaultValue : source.Max(selector); - } - - /// - /// Safety wrapper around Min, returning the default value if source has no elements - /// - /// - /// - /// - /// - /// - /// - public static TResult? MinOrDefault( - this IList source, - Func selector, - TResult? defaultValue) - { - return source.Count == 0 ? defaultValue : source.Min(selector); - } - - public static IEnumerable WhereNotNull(this IEnumerable source) - where TSource : class - { - return source.Where(item => item != null)!; - } -} diff --git a/API/Extensions/FileInfoExtensions.cs b/API/Extensions/FileInfoExtensions.cs deleted file mode 100644 index 1403486dd..000000000 --- a/API/Extensions/FileInfoExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.IO; - -namespace API.Extensions; -#nullable enable - -public static class FileInfoExtensions -{ - /// - /// Checks if the last write time of the file is after the passed date - /// - /// - /// - /// - public static bool HasFileBeenModifiedSince(this FileInfo fileInfo, DateTime comparison) - { - return DateTime.Compare(fileInfo.LastWriteTime, comparison) > 0; - } -} diff --git a/API/Extensions/PathExtensions.cs b/API/Extensions/PathExtensions.cs deleted file mode 100644 index 64c0616ab..000000000 --- a/API/Extensions/PathExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.IO; - -namespace API.Extensions; -#nullable enable - -public static class PathExtensions -{ - public static string GetFullPathWithoutExtension(this string filepath) - { - if (string.IsNullOrEmpty(filepath)) return filepath; - var extension = Path.GetExtension(filepath); - if (string.IsNullOrEmpty(extension)) return filepath; - return Path.GetFullPath(filepath.Replace(extension, string.Empty)); - } -} diff --git a/API/Extensions/QueryExtensions/Filtering/AnnotationFilter.cs b/API/Extensions/QueryExtensions/Filtering/AnnotationFilter.cs deleted file mode 100644 index a98458ba8..000000000 --- a/API/Extensions/QueryExtensions/Filtering/AnnotationFilter.cs +++ /dev/null @@ -1,187 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using API.DTOs.Filtering.v2; -using API.Entities; -using Kavita.Common; -using Microsoft.EntityFrameworkCore; - -namespace API.Extensions.QueryExtensions.Filtering; - -public static class AnnotationFilter -{ - - public static IQueryable IsOwnedBy(this IQueryable queryable, bool condition, - FilterComparison comparison, IList ownerIds) - { - if (ownerIds.Count == 0 || !condition) return queryable; - - return comparison switch - { - FilterComparison.Equal => queryable.Where(a => a.AppUserId == ownerIds[0]), - FilterComparison.Contains => queryable.Where(a => ownerIds.Contains(a.AppUserId)), - FilterComparison.NotContains => queryable.Where(a => !ownerIds.Contains(a.AppUserId)), - FilterComparison.NotEqual => queryable.Where(a => a.AppUserId != ownerIds[0]), - _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), - }; - } - - public static IQueryable IsInLibrary(this IQueryable queryable, bool condition, - FilterComparison comparison, IList libraryIds) - { - if (libraryIds.Count == 0 || !condition) return queryable; - - return comparison switch - { - FilterComparison.Equal => queryable.Where(a => a.LibraryId == libraryIds[0]), - FilterComparison.Contains => queryable.Where(a => libraryIds.Contains(a.LibraryId)), - FilterComparison.NotContains => queryable.Where(a => !libraryIds.Contains(a.LibraryId)), - FilterComparison.NotEqual => queryable.Where(a => a.LibraryId != libraryIds[0]), - _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), - }; - } - - public static IQueryable HasSeries(this IQueryable queryable, bool condition, - FilterComparison comparison, IList seriesIds) - { - if (seriesIds.Count == 0 || !condition) return queryable; - - return comparison switch - { - FilterComparison.Equal => queryable.Where(a => a.SeriesId == seriesIds[0]), - FilterComparison.Contains => queryable.Where(a => seriesIds.Contains(a.SeriesId)), - FilterComparison.NotContains => queryable.Where(a => !seriesIds.Contains(a.SeriesId)), - FilterComparison.NotEqual => queryable.Where(a => a.SeriesId != seriesIds[0]), - _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), - }; - } - - public static IQueryable IsUsingHighlights(this IQueryable queryable, bool condition, - FilterComparison comparison, IList highlightSlotIdxs) - { - if (highlightSlotIdxs.Count == 0 || !condition) return queryable; - - return comparison switch - { - FilterComparison.Equal => queryable.Where(a => a.SelectedSlotIndex == highlightSlotIdxs[0]), - FilterComparison.Contains => queryable.Where(a => highlightSlotIdxs.Contains(a.SelectedSlotIndex)), - FilterComparison.NotContains => queryable.Where(a => !highlightSlotIdxs.Contains(a.SelectedSlotIndex)), - FilterComparison.NotEqual => queryable.Where(a => a.SelectedSlotIndex != highlightSlotIdxs[0]), - _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), - }; - } - - public static IQueryable HasSelected(this IQueryable queryable, bool condition, - FilterComparison comparison, string value) - { - if (string.IsNullOrEmpty(value) || !condition) return queryable; - - return comparison switch - { - FilterComparison.Equal => queryable.Where(a => a.SelectedText == value), - FilterComparison.NotEqual => queryable.Where(a => a.SelectedText != value), - FilterComparison.BeginsWith => queryable.Where(a => EF.Functions.Like(a.SelectedText, $"{value}%")), - FilterComparison.EndsWith => queryable.Where(a => EF.Functions.Like(a.SelectedText, $"%{value}")), - FilterComparison.Matches => queryable.Where(a => EF.Functions.Like(a.SelectedText, $"%{value}%")), - FilterComparison.GreaterThan or - FilterComparison.GreaterThanEqual or - FilterComparison.LessThan or - FilterComparison.LessThanEqual or - FilterComparison.Contains or - FilterComparison.MustContains or - FilterComparison.NotContains or - FilterComparison.IsBefore or - FilterComparison.IsAfter or - FilterComparison.IsInLast or - FilterComparison.IsNotInLast or - FilterComparison.IsEmpty => throw new KavitaException($"{comparison} is not applicable for Annotation.SelectedText"), - _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), - }; - } - - public static IQueryable HasCommented(this IQueryable queryable, bool condition, - FilterComparison comparison, string value) - { - if (string.IsNullOrEmpty(value) || !condition) return queryable; - - return comparison switch - { - FilterComparison.Equal => queryable.Where(a => a.CommentPlainText == value), - FilterComparison.NotEqual => queryable.Where(a => a.CommentPlainText != value), - FilterComparison.BeginsWith => queryable.Where(a => EF.Functions.Like(a.CommentPlainText, $"{value}%")), - FilterComparison.EndsWith => queryable.Where(a => EF.Functions.Like(a.CommentPlainText, $"%{value}")), - FilterComparison.Matches => queryable.Where(a => EF.Functions.Like(a.CommentPlainText, $"%{value}%")), - FilterComparison.GreaterThan or - FilterComparison.GreaterThanEqual or - FilterComparison.LessThan or - FilterComparison.LessThanEqual or - FilterComparison.Contains or - FilterComparison.MustContains or - FilterComparison.NotContains or - FilterComparison.IsBefore or - FilterComparison.IsAfter or - FilterComparison.IsInLast or - FilterComparison.IsNotInLast or - FilterComparison.IsEmpty => throw new KavitaException($"{comparison} is not applicable for Annotation.CommentPlainText"), - _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), - }; - } - - public static IQueryable HasLikes(this IQueryable queryable, bool condition, - FilterComparison comparison, int value) - { - if (!condition) return queryable; - - return comparison switch - { - FilterComparison.Equal => queryable.Where(a => a.Likes.Count == value), - FilterComparison.NotEqual => queryable.Where(a => a.Likes.Count != value), - FilterComparison.GreaterThan => queryable.Where(a => a.Likes.Count > value), - FilterComparison.GreaterThanEqual => queryable.Where(a => a.Likes.Count >= value), - FilterComparison.LessThan => queryable.Where(a => a.Likes.Count < value), - FilterComparison.LessThanEqual => queryable.Where(a => a.Likes.Count <= value), - FilterComparison.BeginsWith or - FilterComparison.EndsWith or - FilterComparison.Matches or - FilterComparison.Contains or - FilterComparison.MustContains or - FilterComparison.NotContains or - FilterComparison.IsBefore or - FilterComparison.IsAfter or - FilterComparison.IsInLast or - FilterComparison.IsNotInLast or - FilterComparison.IsEmpty => throw new KavitaException($"{comparison} is not applicable for Annotation.Likes"), - _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), - }; - } - - public static IQueryable IsLikedBy(this IQueryable queryable, bool condition, - FilterComparison comparison, IList value) - { - if (value.Count == 0 || !condition) return queryable; - - return comparison switch - { - FilterComparison.Equal => queryable.Where(a => a.Likes.Contains(value[0])), - FilterComparison.NotEqual => queryable.Where(a => a!.Likes.Contains(value[0])), - FilterComparison.Contains => queryable.Where(a => a.Likes.Any(value.Contains)), - FilterComparison.NotContains => queryable.Where(a => !a.Likes.Any(value.Contains)), - FilterComparison.GreaterThan or - FilterComparison.GreaterThanEqual or - FilterComparison.LessThan or - FilterComparison.LessThanEqual or - FilterComparison.BeginsWith or - FilterComparison.EndsWith or - FilterComparison.Matches or - FilterComparison.MustContains or - FilterComparison.IsBefore or - FilterComparison.IsAfter or - FilterComparison.IsInLast or - FilterComparison.IsNotInLast or - FilterComparison.IsEmpty => throw new KavitaException($"{comparison} is not applicable for Annotation.Likes"), - _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), - }; - } - - -} diff --git a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs b/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs deleted file mode 100644 index 49b8c1de4..000000000 --- a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs +++ /dev/null @@ -1,945 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using API.DTOs.Filtering.v2; -using API.Entities; -using API.Entities.Enums; -using API.Services.Tasks.Scanner.Parser; -using Kavita.Common; -using Microsoft.EntityFrameworkCore; - -namespace API.Extensions.QueryExtensions.Filtering; -#nullable enable - -public static class SeriesFilter -{ - private const float FloatingPointTolerance = 0.001f; - - public static IQueryable HasLanguage(this IQueryable queryable, bool condition, - FilterComparison comparison, IList languages) - { - if (languages.Count == 0 || !condition) return queryable; - - switch (comparison) - { - case FilterComparison.Equal: - return queryable.Where(s => s.Metadata.Language.Equals(languages[0])); - case FilterComparison.Contains: - return queryable.Where(s => languages.Contains(s.Metadata.Language)); - case FilterComparison.MustContains: - return queryable.Where(s => languages.All(s2 => s2.Equals(s.Metadata.Language))); - case FilterComparison.NotContains: - return queryable.Where(s => !languages.Contains(s.Metadata.Language)); - case FilterComparison.NotEqual: - return queryable.Where(s => !s.Metadata.Language.Equals(languages[0])); - case FilterComparison.Matches: - return queryable.Where(s => EF.Functions.Like(s.Metadata.Language, $"{languages[0]}%")); - case FilterComparison.GreaterThan: - case FilterComparison.GreaterThanEqual: - case FilterComparison.LessThan: - case FilterComparison.LessThanEqual: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.IsEmpty: - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - } - - public static IQueryable HasReleaseYear(this IQueryable queryable, bool condition, - FilterComparison comparison, int? releaseYear) - { - if (!condition || releaseYear == null) return queryable; - - switch (comparison) - { - case FilterComparison.Equal: - return queryable.Where(s => s.Metadata.ReleaseYear == releaseYear); - case FilterComparison.GreaterThan: - case FilterComparison.IsAfter: - return queryable.Where(s => s.Metadata.ReleaseYear > releaseYear); - case FilterComparison.GreaterThanEqual: - return queryable.Where(s => s.Metadata.ReleaseYear >= releaseYear); - case FilterComparison.LessThan: - case FilterComparison.IsBefore: - return queryable.Where(s => s.Metadata.ReleaseYear < releaseYear); - case FilterComparison.LessThanEqual: - return queryable.Where(s => s.Metadata.ReleaseYear <= releaseYear); - case FilterComparison.IsInLast: - return queryable.Where(s => s.Metadata.ReleaseYear >= DateTime.Now.Year - (int) releaseYear); - case FilterComparison.IsNotInLast: - return queryable.Where(s => s.Metadata.ReleaseYear < DateTime.Now.Year - (int) releaseYear); - case FilterComparison.IsEmpty: - return queryable.Where(s => s.Metadata.ReleaseYear == 0); - case FilterComparison.Matches: - case FilterComparison.Contains: - case FilterComparison.NotContains: - case FilterComparison.NotEqual: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.MustContains: - throw new KavitaException($"{comparison} not applicable for Series.ReleaseYear"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - } - - - public static IQueryable HasRating(this IQueryable queryable, bool condition, - FilterComparison comparison, float rating, int userId) - { - if (rating < 0 || !condition || userId <= 0) return queryable; - - // AppUserRating stores a 5-digit number. - rating = Math.Clamp(rating, 0f, 5f); - - - switch (comparison) - { - case FilterComparison.Equal: - return queryable.Where(s => s.Ratings.Any(r => Math.Abs(r.Rating - rating) <= FloatingPointTolerance && r.AppUserId == userId)); - case FilterComparison.GreaterThan: - return queryable.Where(s => s.Ratings.Any(r => r.Rating > rating && r.AppUserId == userId)); - case FilterComparison.GreaterThanEqual: - return queryable.Where(s => s.Ratings.Any(r => r.Rating >= rating && r.AppUserId == userId)); - case FilterComparison.LessThan: - return queryable.Where(s => s.Ratings.Any(r => r.Rating < rating && r.AppUserId == userId)); - case FilterComparison.LessThanEqual: - return queryable.Where(s => s.Ratings.Any(r => r.Rating <= rating && r.AppUserId == userId)); - case FilterComparison.NotEqual: - return queryable.Where(s => s.Ratings.Any(r => Math.Abs(r.Rating - rating) >= FloatingPointTolerance && r.AppUserId == userId)); - case FilterComparison.IsEmpty: - return queryable.Where(s => s.Ratings.All(r => r.AppUserId != userId)); - case FilterComparison.Contains: - case FilterComparison.Matches: - 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.Rating"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - } - - public static IQueryable HasAgeRating(this IQueryable queryable, bool condition, - FilterComparison comparison, IList ratings) - { - if (!condition || ratings.Count == 0) return queryable; - - var firstRating = ratings[0]; - switch (comparison) - { - case FilterComparison.Equal: - return queryable.Where(s => s.Metadata.AgeRating == firstRating); - case FilterComparison.GreaterThan: - return queryable.Where(s => s.Metadata.AgeRating > firstRating); - case FilterComparison.GreaterThanEqual: - return queryable.Where(s => s.Metadata.AgeRating >= firstRating); - case FilterComparison.LessThan: - return queryable.Where(s => s.Metadata.AgeRating < firstRating); - case FilterComparison.LessThanEqual: - return queryable.Where(s => s.Metadata.AgeRating <= firstRating); - case FilterComparison.Contains: - return queryable.Where(s => ratings.Contains(s.Metadata.AgeRating)); - case FilterComparison.NotContains: - return queryable.Where(s => !ratings.Contains(s.Metadata.AgeRating)); - case FilterComparison.NotEqual: - return queryable.Where(s => s.Metadata.AgeRating != firstRating); - case FilterComparison.Matches: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.MustContains: - case FilterComparison.IsEmpty: - throw new KavitaException($"{comparison} not applicable for Series.AgeRating"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - } - - public static IQueryable HasAverageReadTime(this IQueryable queryable, bool condition, - FilterComparison comparison, int avgReadTime) - { - if (!condition || avgReadTime < 0) return queryable; - - switch (comparison) - { - case FilterComparison.NotEqual: - return queryable.WhereNotEqual(s => s.AvgHoursToRead, avgReadTime); - case FilterComparison.Equal: - return queryable.WhereEqual(s => s.AvgHoursToRead, avgReadTime); - case FilterComparison.GreaterThan: - return queryable.WhereGreaterThan(s => s.AvgHoursToRead, avgReadTime); - case FilterComparison.GreaterThanEqual: - return queryable.WhereGreaterThanOrEqual(s => s.AvgHoursToRead, avgReadTime); - case FilterComparison.LessThan: - return queryable.WhereLessThan(s => s.AvgHoursToRead, avgReadTime); - case FilterComparison.LessThanEqual: - return queryable.WhereLessThanOrEqual(s => s.AvgHoursToRead, avgReadTime); - case FilterComparison.Contains: - case FilterComparison.Matches: - case FilterComparison.NotContains: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.MustContains: - case FilterComparison.IsEmpty: - throw new KavitaException($"{comparison} not applicable for Series.AverageReadTime"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - } - - public static IQueryable HasPublicationStatus(this IQueryable queryable, bool condition, - FilterComparison comparison, IList pubStatues) - { - if (!condition || pubStatues.Count == 0) return queryable; - - var firstStatus = pubStatues[0]; - switch (comparison) - { - case FilterComparison.Equal: - return queryable.Where(s => s.Metadata.PublicationStatus == firstStatus); - case FilterComparison.Contains: - return queryable.Where(s => pubStatues.Contains(s.Metadata.PublicationStatus)); - case FilterComparison.NotContains: - return queryable.Where(s => !pubStatues.Contains(s.Metadata.PublicationStatus)); - case FilterComparison.NotEqual: - return queryable.Where(s => s.Metadata.PublicationStatus != firstStatus); - case FilterComparison.MustContains: - case FilterComparison.GreaterThan: - case FilterComparison.GreaterThanEqual: - case FilterComparison.LessThan: - case FilterComparison.LessThanEqual: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.Matches: - case FilterComparison.IsEmpty: - throw new KavitaException($"{comparison} not applicable for Series.PublicationStatus"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - } - - /// - /// - /// - /// This is more taxing on memory as the percentage calculation must be done in Memory - /// - /// - public static IQueryable HasReadingProgress(this IQueryable queryable, bool condition, - FilterComparison comparison, float readProgress, int userId) - { - if (!condition) return queryable; - - var subQuery = queryable - .Select(s => new - { - SeriesId = s.Id, - SeriesName = s.Name, - Percentage = s.Progress - .Where(p => p != null && p.AppUserId == userId) - .Sum(p => p != null ? (p.PagesRead * 1.0f / s.Pages) : 0f) * 100f - }) - .AsSplitQuery(); - - switch (comparison) - { - case FilterComparison.Equal: - subQuery = subQuery.WhereEqual(s => s.Percentage, readProgress); - break; - case FilterComparison.GreaterThan: - subQuery = subQuery.WhereGreaterThan(s => s.Percentage, readProgress); - break; - case FilterComparison.GreaterThanEqual: - subQuery = subQuery.WhereGreaterThanOrEqual(s => s.Percentage, readProgress); - break; - case FilterComparison.LessThan: - subQuery = subQuery.WhereLessThan(s => s.Percentage, readProgress); - break; - case FilterComparison.LessThanEqual: - subQuery = subQuery.WhereLessThanOrEqual(s => s.Percentage, readProgress); - break; - case FilterComparison.NotEqual: - subQuery = subQuery.WhereNotEqual(s => s.Percentage, readProgress); - break; - case FilterComparison.IsEmpty: - 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.ReadProgress"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - - var ids = subQuery.Select(s => s.SeriesId); - 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 - { - SeriesId = s.Id, - SeriesName = s.Name, - AverageRating = s.ExternalSeriesMetadata.AverageExternalRating - }) - .AsSplitQuery() - .AsQueryable(); - - switch (comparison) - { - case FilterComparison.Equal: - subQuery = subQuery.WhereEqual(s => s.AverageRating, rating); - break; - case FilterComparison.GreaterThan: - subQuery = subQuery.WhereGreaterThan(s => s.AverageRating, rating); - break; - case FilterComparison.GreaterThanEqual: - subQuery = subQuery.WhereGreaterThanOrEqual(s => s.AverageRating, rating); - break; - case FilterComparison.LessThan: - subQuery = subQuery.WhereLessThan(s => s.AverageRating, rating); - break; - case FilterComparison.LessThanEqual: - subQuery = subQuery.WhereLessThanOrEqual(s => s.AverageRating, rating); - break; - case FilterComparison.NotEqual: - subQuery = subQuery.WhereNotEqual(s => s.AverageRating, rating); - 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: - case FilterComparison.IsEmpty: - throw new KavitaException($"{comparison} not applicable for Series.AverageRating"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - - var ids = subQuery.Select(s => s.SeriesId); - return queryable.Where(s => ids.Contains(s.Id)); - } - - /// - /// HasReadingDate but used to filter where last reading point was TODAY() - timeDeltaDays. This allows the user - /// to build smart filters "Haven't read in a month" - /// - public static IQueryable HasReadLast(this IQueryable queryable, bool condition, - FilterComparison comparison, int timeDeltaDays, int userId) - { - if (!condition || timeDeltaDays == 0) return queryable; - - var subQuery = queryable - .Include(s => s.Progress) - .Where(s => s.Progress.Any()) - .Select(s => new - { - SeriesId = s.Id, - SeriesName = s.Name, - MaxDate = s.Progress.Where(p => p != null && p.AppUserId == userId) - .Select(p => (DateTime?) p.LastModified) - .DefaultIfEmpty() - .Max() - }) - .Where(s => s.MaxDate != null) - .AsSplitQuery() - .AsEnumerable(); - - var date = DateTime.Now.AddDays(-timeDeltaDays); - - switch (comparison) - { - case FilterComparison.Equal: - subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate.Equals(date)); - break; - case FilterComparison.IsAfter: - case FilterComparison.GreaterThan: - subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate > date); - break; - case FilterComparison.GreaterThanEqual: - subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate >= date); - break; - case FilterComparison.IsBefore: - case FilterComparison.LessThan: - subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate < date); - break; - case FilterComparison.LessThanEqual: - subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate <= date); - break; - case FilterComparison.NotEqual: - subQuery = subQuery.Where(s => s.MaxDate != null && !s.MaxDate.Equals(date)); - break; - case FilterComparison.Matches: - case FilterComparison.Contains: - case FilterComparison.NotContains: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.MustContains: - case FilterComparison.IsEmpty: - throw new KavitaException($"{comparison} not applicable for Series.ReadProgress"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - - var ids = subQuery.Select(s => s.SeriesId); - return queryable.Where(s => ids.Contains(s.Id)); - } - - public static IQueryable HasReadingDate(this IQueryable queryable, bool condition, - FilterComparison comparison, DateTime? date, int userId) - { - if (!condition || !date.HasValue) return queryable; - - var subQuery = queryable - .Include(s => s.Progress) - .Where(s => s.Progress.Any()) - .Select(s => new - { - SeriesId = s.Id, - SeriesName = s.Name, - MaxDate = s.Progress.Where(p => p != null && p.AppUserId == userId) - .Select(p => (DateTime?) p.LastModified) - .DefaultIfEmpty() - .Max() - }) - .Where(s => s.MaxDate != null) - .AsSplitQuery() - .AsEnumerable(); - - switch (comparison) - { - case FilterComparison.Equal: - subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate.Equals(date)); - break; - case FilterComparison.IsAfter: - case FilterComparison.GreaterThan: - subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate > date); - break; - case FilterComparison.GreaterThanEqual: - subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate >= date); - break; - case FilterComparison.IsBefore: - case FilterComparison.LessThan: - subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate < date); - break; - case FilterComparison.LessThanEqual: - subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate <= date); - break; - case FilterComparison.NotEqual: - subQuery = subQuery.Where(s => s.MaxDate != null && !s.MaxDate.Equals(date)); - break; - case FilterComparison.Matches: - case FilterComparison.Contains: - case FilterComparison.NotContains: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.MustContains: - case FilterComparison.IsEmpty: - throw new KavitaException($"{comparison} not applicable for Series.ReadProgress"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - - var ids = subQuery.Select(s => s.SeriesId); - return queryable.Where(s => ids.Contains(s.Id)); - } - - public static IQueryable HasTags(this IQueryable queryable, bool condition, - FilterComparison comparison, IList tags) - { - if (!condition || (comparison != FilterComparison.IsEmpty && tags.Count == 0)) return queryable; - - switch (comparison) - { - case FilterComparison.Equal: - case FilterComparison.Contains: - return queryable.Where(s => s.Metadata.Tags.Any(t => tags.Contains(t.Id))); - case FilterComparison.NotEqual: - case FilterComparison.NotContains: - return queryable.Where(s => s.Metadata.Tags.All(t => !tags.Contains(t.Id))); - case FilterComparison.MustContains: - // Deconstruct and do a Union of a bunch of where statements since this doesn't translate - var queries = new List>() - { - queryable - }; - queries.AddRange(tags.Select(gId => queryable.Where(s => s.Metadata.Tags.Any(p => p.Id == gId)))); - - return queries.Aggregate((q1, q2) => q1.Intersect(q2)); - case FilterComparison.IsEmpty: - return queryable.Where(s => s.Metadata.Tags.Count == 0); - case FilterComparison.GreaterThan: - case FilterComparison.GreaterThanEqual: - case FilterComparison.LessThan: - case FilterComparison.LessThanEqual: - case FilterComparison.Matches: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - throw new KavitaException($"{comparison} not applicable for Series.Tags"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - } - - public static IQueryable HasPeople(this IQueryable queryable, bool condition, - FilterComparison comparison, IList people, PersonRole role) - { - if (!condition || (comparison != FilterComparison.IsEmpty && people.Count == 0)) return queryable; - - switch (comparison) - { - case FilterComparison.Equal: - case FilterComparison.Contains: - return queryable.Where(s => s.Metadata.People.Any(p => people.Contains(p.PersonId) && p.Role == role)); - case FilterComparison.NotEqual: - case FilterComparison.NotContains: - return queryable.Where(s => s.Metadata.People.All(p => !people.Contains(p.PersonId) || p.Role != role)); - case FilterComparison.MustContains: - var queries = new List>() - { - queryable - }; - queries.AddRange(people.Select(personId => - queryable.Where(s => s.Metadata.People.Any(p => p.PersonId == personId && p.Role == role)))); - - return queries.Aggregate((q1, q2) => q1.Intersect(q2)); - case FilterComparison.IsEmpty: - // Ensure no person with the given role exists - return queryable.Where(s => s.Metadata.People.All(p => p.Role != role)); - case FilterComparison.GreaterThan: - case FilterComparison.GreaterThanEqual: - case FilterComparison.LessThan: - case FilterComparison.LessThanEqual: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.Matches: - throw new KavitaException($"{comparison} not applicable for Series.People"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - } - - public static IQueryable HasPeopleLegacy(this IQueryable queryable, bool condition, - FilterComparison comparison, IList people) - { - if (!condition || people.Count == 0) return queryable; - - switch (comparison) - { - case FilterComparison.Equal: - case FilterComparison.Contains: - return queryable.Where(s => s.Metadata.People.Any(p => people.Contains(p.PersonId))); - case FilterComparison.NotEqual: - case FilterComparison.NotContains: - return queryable.Where(s => s.Metadata.People.All(t => !people.Contains(t.PersonId))); - case FilterComparison.MustContains: - // Deconstruct and do a Union of a bunch of where statements since this doesn't translate - var queries = new List>() - { - queryable - }; - queries.AddRange(people.Select(gId => queryable.Where(s => s.Metadata.People.Any(p => p.PersonId == gId)))); - - return queries.Aggregate((q1, q2) => q1.Intersect(q2)); - case FilterComparison.IsEmpty: - case FilterComparison.GreaterThan: - case FilterComparison.GreaterThanEqual: - case FilterComparison.LessThan: - case FilterComparison.LessThanEqual: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.Matches: - throw new KavitaException($"{comparison} not applicable for Series.People"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - } - - public static IQueryable HasGenre(this IQueryable queryable, bool condition, - FilterComparison comparison, IList genres) - { - if (!condition || (comparison != FilterComparison.IsEmpty && genres.Count == 0)) return queryable; - - switch (comparison) - { - case FilterComparison.Equal: - case FilterComparison.Contains: - return queryable.Where(s => s.Metadata.Genres.Any(p => genres.Contains(p.Id))); - case FilterComparison.NotEqual: - case FilterComparison.NotContains: - return queryable.Where(s => s.Metadata.Genres.All(p => !genres.Contains(p.Id))); - case FilterComparison.MustContains: - // Deconstruct and do a Union of a bunch of where statements since this doesn't translate - var queries = new List>() - { - queryable - }; - queries.AddRange(genres.Select(gId => queryable.Where(s => s.Metadata.Genres.Any(p => p.Id == gId)))); - - return queries.Aggregate((q1, q2) => q1.Intersect(q2)); - case FilterComparison.IsEmpty: - return queryable.Where(s => s.Metadata.Genres.Count == 0); - case FilterComparison.GreaterThan: - case FilterComparison.GreaterThanEqual: - case FilterComparison.LessThan: - case FilterComparison.LessThanEqual: - case FilterComparison.Matches: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - throw new KavitaException($"{comparison} not applicable for Series.Genres"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - } - - public static IQueryable HasFormat(this IQueryable queryable, bool condition, - FilterComparison comparison, IList formats) - { - if (!condition || formats.Count == 0) return queryable; - - switch (comparison) - { - case FilterComparison.Equal: - case FilterComparison.Contains: - return queryable.Where(s => formats.Contains(s.Format)); - case FilterComparison.NotContains: - case FilterComparison.NotEqual: - return queryable.Where(s => !formats.Contains(s.Format)); - case FilterComparison.MustContains: - case FilterComparison.GreaterThan: - case FilterComparison.GreaterThanEqual: - case FilterComparison.LessThan: - case FilterComparison.LessThanEqual: - case FilterComparison.Matches: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.IsEmpty: - throw new KavitaException($"{comparison} not applicable for Series.Format"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - } - - public static IQueryable HasCollectionTags(this IQueryable queryable, bool condition, - FilterComparison comparison, IList collectionTags, IList collectionSeries) - { - if (!condition || (comparison != FilterComparison.IsEmpty && collectionTags.Count == 0)) return queryable; - - - switch (comparison) - { - case FilterComparison.Equal: - case FilterComparison.Contains: - return queryable.Where(s => collectionSeries.Contains(s.Id)); - case FilterComparison.NotContains: - case FilterComparison.NotEqual: - return queryable.Where(s => !collectionSeries.Contains(s.Id)); - case FilterComparison.MustContains: - // // Deconstruct and do a Union of a bunch of where statements since this doesn't translate - var queries = new List>() - { - queryable - }; - queries.AddRange(collectionSeries.Select(gId => queryable.Where(s => collectionSeries.Any(p => p == s.Id)))); - - return queries.Aggregate((q1, q2) => q1.Intersect(q2)); - case FilterComparison.IsEmpty: - return queryable.Where(s => s.Collections.Count == 0); - case FilterComparison.GreaterThan: - case FilterComparison.GreaterThanEqual: - case FilterComparison.LessThan: - case FilterComparison.LessThanEqual: - case FilterComparison.Matches: - case FilterComparison.BeginsWith: - case FilterComparison.EndsWith: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - throw new KavitaException($"{comparison} not applicable for Series.CollectionTags"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); - } - } - - public static IQueryable HasName(this IQueryable queryable, bool condition, - FilterComparison comparison, string queryString) - { - if (string.IsNullOrEmpty(queryString) || !condition) return queryable; - - switch (comparison) - { - case FilterComparison.Equal: - return queryable.Where(s => s.Name.Equals(queryString) - || s.OriginalName.Equals(queryString) - || s.LocalizedName.Equals(queryString) - || s.SortName.Equals(queryString)); - case FilterComparison.BeginsWith: - return queryable.Where(s => EF.Functions.Like(s.Name, $"{queryString}%") - ||EF.Functions.Like(s.OriginalName, $"{queryString}%") - || EF.Functions.Like(s.LocalizedName, $"{queryString}%") - || EF.Functions.Like(s.SortName, $"{queryString}%")); - case FilterComparison.EndsWith: - return queryable.Where(s => EF.Functions.Like(s.Name, $"%{queryString}") - ||EF.Functions.Like(s.OriginalName, $"%{queryString}") - || EF.Functions.Like(s.LocalizedName, $"%{queryString}") - || EF.Functions.Like(s.SortName, $"%{queryString}")); - case FilterComparison.Matches: - return queryable.Where(s => EF.Functions.Like(s.Name, $"%{queryString}%") - ||EF.Functions.Like(s.OriginalName, $"%{queryString}%") - || EF.Functions.Like(s.LocalizedName, $"%{queryString}%") - || EF.Functions.Like(s.SortName, $"%{queryString}%")); - case FilterComparison.NotEqual: - return queryable.Where(s => s.Name != queryString - || s.OriginalName != queryString - || s.LocalizedName != queryString - || s.SortName != queryString); - case FilterComparison.NotContains: - case FilterComparison.GreaterThan: - case FilterComparison.GreaterThanEqual: - case FilterComparison.LessThan: - case FilterComparison.LessThanEqual: - case FilterComparison.Contains: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.MustContains: - case FilterComparison.IsEmpty: - throw new KavitaException($"{comparison} not applicable for Series.Name"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"); - } - } - - public static IQueryable HasSummary(this IQueryable queryable, bool condition, - FilterComparison comparison, string queryString) - { - if (!condition) return queryable; - - switch (comparison) - { - case FilterComparison.Equal: - return queryable.Where(s => s.Metadata.Summary.Equals(queryString)); - case FilterComparison.BeginsWith: - return queryable.Where(s => EF.Functions.Like(s.Metadata.Summary, $"{queryString}%")); - case FilterComparison.EndsWith: - return queryable.Where(s => EF.Functions.Like(s.Metadata.Summary, $"%{queryString}")); - case FilterComparison.Matches: - return queryable.Where(s => EF.Functions.Like(s.Metadata.Summary, $"%{queryString}%")); - case FilterComparison.NotEqual: - return queryable.Where(s => s.Metadata.Summary != queryString); - case FilterComparison.IsEmpty: - return queryable.Where(s => string.IsNullOrEmpty(s.Metadata.Summary)); - case FilterComparison.NotContains: - case FilterComparison.GreaterThan: - case FilterComparison.GreaterThanEqual: - case FilterComparison.LessThan: - case FilterComparison.LessThanEqual: - case FilterComparison.Contains: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.MustContains: - throw new KavitaException($"{comparison} not applicable for Series.Metadata.Summary"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"); - } - } - - public static IQueryable HasPath(this IQueryable queryable, bool condition, - FilterComparison comparison, string queryString) - { - if (!condition) return queryable; - - var normalizedPath = Parser.NormalizePath(queryString); - - switch (comparison) - { - case FilterComparison.Equal: - return queryable.Where(s => s.FolderPath != null && s.FolderPath.Equals(normalizedPath)); - case FilterComparison.BeginsWith: - return queryable.Where(s => s.FolderPath != null && EF.Functions.Like(s.FolderPath, $"{normalizedPath}%")); - case FilterComparison.EndsWith: - return queryable.Where(s => s.FolderPath != null && EF.Functions.Like(s.FolderPath, $"%{normalizedPath}")); - case FilterComparison.Matches: - return queryable.Where(s => s.FolderPath != null && EF.Functions.Like(s.FolderPath, $"%{normalizedPath}%")); - case FilterComparison.NotEqual: - return queryable.Where(s => s.FolderPath != null && s.FolderPath != normalizedPath); - case FilterComparison.NotContains: - case FilterComparison.GreaterThan: - case FilterComparison.GreaterThanEqual: - case FilterComparison.LessThan: - case FilterComparison.LessThanEqual: - case FilterComparison.Contains: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.MustContains: - case FilterComparison.IsEmpty: - throw new KavitaException($"{comparison} not applicable for Series.FolderPath"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"); - } - } - - public static IQueryable HasFilePath(this IQueryable queryable, bool condition, - FilterComparison comparison, string queryString) - { - if (!condition) return queryable; - - var normalizedPath = Parser.NormalizePath(queryString); - - switch (comparison) - { - case FilterComparison.Equal: - return queryable.Where(s => - s.Volumes.Any(v => - v.Chapters.Any(c => - c.Files.Any(f => - f.FilePath != null && f.FilePath.Equals(normalizedPath) - ) - ) - ) - ); - case FilterComparison.BeginsWith: - return queryable.Where(s => - s.Volumes.Any(v => - v.Chapters.Any(c => - c.Files.Any(f => - f.FilePath != null && EF.Functions.Like(f.FilePath, $"{normalizedPath}%") - ) - ) - ) - ); - case FilterComparison.EndsWith: - return queryable.Where(s => - s.Volumes.Any(v => - v.Chapters.Any(c => - c.Files.Any(f => - f.FilePath != null && EF.Functions.Like(f.FilePath, $"%{normalizedPath}") - ) - ) - ) - ); - case FilterComparison.Matches: - return queryable.Where(s => - s.Volumes.Any(v => - v.Chapters.Any(c => - c.Files.Any(f => - f.FilePath != null && EF.Functions.Like(f.FilePath, $"%{normalizedPath}%") - ) - ) - ) - ); - case FilterComparison.NotEqual: - return queryable.Where(s => - s.Volumes.Any(v => - v.Chapters.Any(c => - c.Files.Any(f => - f.FilePath == null || !f.FilePath.Equals(normalizedPath) - ) - ) - ) - ); - case FilterComparison.NotContains: - case FilterComparison.GreaterThan: - case FilterComparison.GreaterThanEqual: - case FilterComparison.LessThan: - case FilterComparison.LessThanEqual: - case FilterComparison.Contains: - case FilterComparison.IsBefore: - case FilterComparison.IsAfter: - case FilterComparison.IsInLast: - case FilterComparison.IsNotInLast: - case FilterComparison.MustContains: - case FilterComparison.IsEmpty: - throw new KavitaException($"{comparison} not applicable for Series.FolderPath"); - default: - throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"); - } - } - - public static IQueryable HasFileSize(this IQueryable queryable, bool condition, - FilterComparison comparison, float fileSize) - { - if (fileSize == 0f || !condition) return queryable; - - return comparison switch - { - FilterComparison.Equal => queryable.Where(s => s.Volumes.Sum(v => v.Chapters.Sum(c => c.Files.Sum(f => f.Bytes))) == fileSize), - FilterComparison.LessThan => queryable.Where(s => s.Volumes.Sum(v => v.Chapters.Sum(c => c.Files.Sum(f => f.Bytes))) < fileSize), - FilterComparison.LessThanEqual => queryable.Where(s => s.Volumes.Sum(v => v.Chapters.Sum(c => c.Files.Sum(f => f.Bytes))) <= fileSize), - FilterComparison.GreaterThan => queryable.Where(s => s.Volumes.Sum(v => v.Chapters.Sum(c => c.Files.Sum(f => f.Bytes))) > fileSize), - FilterComparison.GreaterThanEqual => queryable.Where(s => s.Volumes.Sum(v => v.Chapters.Sum(c => c.Files.Sum(f => f.Bytes))) >= fileSize), - _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"), - }; - } - - -} diff --git a/API/Extensions/QueryExtensions/ProjectToExtensions.cs b/API/Extensions/QueryExtensions/ProjectToExtensions.cs deleted file mode 100644 index 067686ad1..000000000 --- a/API/Extensions/QueryExtensions/ProjectToExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ - -using System.Linq; -using AutoMapper; -using AutoMapper.QueryableExtensions; - -namespace API.Extensions.QueryExtensions; - -public static class ProjectToExtensions -{ - public static IQueryable ProjectToWithProgress( - this IQueryable queryable, - IConfigurationProvider config, - int userId) - { - return queryable.ProjectTo(config, new { userId }); - } - - // Convenience overload taking IMapper directly - public static IQueryable ProjectToWithProgress( - this IQueryable queryable, - IMapper mapper, - int userId) - { - return queryable.ProjectTo(mapper.ConfigurationProvider, new { userId }); - } -} diff --git a/API/Extensions/StringExtensions.cs b/API/Extensions/StringExtensions.cs deleted file mode 100644 index 2fdaf52a1..000000000 --- a/API/Extensions/StringExtensions.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text.RegularExpressions; - -namespace API.Extensions; -#nullable enable - -public static partial class StringExtensions -{ - private static readonly Regex SentenceCaseRegex = new(@"(^[a-z])|\.\s+(.)", - RegexOptions.ExplicitCapture | RegexOptions.Compiled, - Services.Tasks.Scanner.Parser.Parser.RegexTimeout); - - public static string Sanitize(this string input) - { - if (string.IsNullOrEmpty(input)) - return string.Empty; - - // Remove all newline and control characters - var sanitized = input - .Replace(Environment.NewLine, string.Empty) - .Replace("\n", string.Empty) - .Replace("\r", string.Empty); - - // Optionally remove other potentially unwanted characters - sanitized = Regex.Replace(sanitized, @"[^\u0020-\u007E]", string.Empty); // Removes non-printable ASCII - - return sanitized.Trim(); // Trim any leading/trailing whitespace - } - - public static string SentenceCase(this string value) - { - return SentenceCaseRegex.Replace(value.ToLower(), s => s.Value.ToUpper()); - } - - /// - /// Apply normalization on the String - /// - /// - /// - public static string ToNormalized(this string? value) - { - return string.IsNullOrEmpty(value) ? string.Empty : Services.Tasks.Scanner.Parser.Parser.Normalize(value); - } - - public static float AsFloat(this string? value, float defaultValue = 0.0f) - { - return string.IsNullOrEmpty(value) ? defaultValue : float.Parse(value, CultureInfo.InvariantCulture); - } - - public static double AsDouble(this string? value, double defaultValue = 0.0f) - { - return string.IsNullOrEmpty(value) ? defaultValue : double.Parse(value, CultureInfo.InvariantCulture); - } - - public static string TrimPrefix(this string? value, string prefix) - { - if (string.IsNullOrEmpty(value)) return string.Empty; - - if (!value.StartsWith(prefix)) return value; - - return value.Substring(prefix.Length); - } - - /// - /// Censor the input string by removing all but the first and last char. - /// - /// - /// - /// If the input is an email (contains @), the domain will remain untouched - public static string Censor(this string? input) - { - if (string.IsNullOrWhiteSpace(input)) return input ?? string.Empty; - - var atIdx = input.IndexOf('@'); - if (atIdx == -1) - { - return $"{input[0]}{new string('*', input.Length - 1)}"; - } - - return input[0] + new string('*', atIdx - 1) + input[atIdx..]; - } - - /// - /// Repeat returns a string that is equal to the original string repeat n times - /// - /// String to repeat - /// Amount of times to repeat - /// - public static string Repeat(this string? input, int n) - { - return string.IsNullOrEmpty(input) ? string.Empty : string.Concat(Enumerable.Repeat(input, n)); - } - - public static IList ParseIntArray(this string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return []; - } - - return value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) - .Select(int.Parse) - .ToList(); - } - - /// - /// Parses a human-readable file size string (e.g. "1.43 GB") into bytes. - /// - /// The input string like "1.43 GB", "4.2 KB", "512 B" - /// Byte count as long - public static long ParseHumanReadableBytes(this string input) - { - if (string.IsNullOrWhiteSpace(input)) - { - throw new ArgumentException("Input cannot be null or empty.", nameof(input)); - } - - - var match = HumanReadableBytesRegex().Match(input); - if (!match.Success) - { - throw new FormatException($"Invalid format: '{input}'"); - } - - - var value = double.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); - var unit = match.Groups[2].Value.ToUpperInvariant(); - - var multiplier = unit switch - { - "B" => 1L, - "KB" => 1L << 10, - "MB" => 1L << 20, - "GB" => 1L << 30, - "TB" => 1L << 40, - "PB" => 1L << 50, - "EB" => 1L << 60, - _ => throw new FormatException($"Unknown unit: '{unit}'") - }; - - return (long)(value * multiplier); - } - - [GeneratedRegex(@"^\s*(\d+(?:\.\d+)?)\s*([KMGTPE]?B)\s*$", RegexOptions.IgnoreCase)] - private static partial Regex HumanReadableBytesRegex(); -} diff --git a/API/Helpers/Builders/MediaErrorBuilder.cs b/API/Helpers/Builders/MediaErrorBuilder.cs deleted file mode 100644 index 4d0f7f3a0..000000000 --- a/API/Helpers/Builders/MediaErrorBuilder.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.IO; -using API.Entities; -using API.Services.Tasks.Scanner.Parser; - -namespace API.Helpers.Builders; - -public class MediaErrorBuilder : IEntityBuilder -{ - private readonly MediaError _mediaError; - public MediaError Build() => _mediaError; - - public MediaErrorBuilder(string filePath) - { - _mediaError = new MediaError() - { - FilePath = Parser.NormalizePath(filePath), - Extension = Path.GetExtension(filePath).Replace(".", string.Empty).ToUpperInvariant() - }; - } - - public MediaErrorBuilder WithComment(string comment) - { - _mediaError.Comment = comment.Trim(); - return this; - } - - public MediaErrorBuilder WithDetails(string details) - { - _mediaError.Details = details.Trim(); - return this; - } -} diff --git a/API/Helpers/Builders/PlusSeriesDtoBuilder.cs b/API/Helpers/Builders/PlusSeriesDtoBuilder.cs deleted file mode 100644 index db84f33c6..000000000 --- a/API/Helpers/Builders/PlusSeriesDtoBuilder.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Linq; -using API.DTOs.Scrobbling; -using API.Entities; -using API.Extensions; -using API.Services.Plus; - -namespace API.Helpers.Builders; - -public class PlusSeriesDtoBuilder : IEntityBuilder -{ - private readonly PlusSeriesRequestDto _seriesRequestDto; - public PlusSeriesRequestDto Build() => _seriesRequestDto; - - /// - /// This must be a FULL Series - /// - /// - public PlusSeriesDtoBuilder(Series series) - { - _seriesRequestDto = new PlusSeriesRequestDto() - { - MediaFormat = series.Library.Type.ConvertToPlusMediaFormat(series.Format), - 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 - }; - } - -} diff --git a/API/Helpers/PagedList.cs b/API/Helpers/PagedList.cs deleted file mode 100644 index 4ab566e5c..000000000 --- a/API/Helpers/PagedList.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; - -namespace API.Helpers; -#nullable enable - -public class PagedList : List -{ - private PagedList(IEnumerable items, int count, int pageNumber, int pageSize) - { - CurrentPage = pageNumber; - TotalPages = (int) Math.Ceiling(count / (double) pageSize); - PageSize = pageSize; - TotalCount = count; - AddRange(items); - } - - public int CurrentPage { get; set; } - public int TotalPages { get; set; } - public int PageSize { get; set; } - public int TotalCount { get; set; } - - public static async Task> CreateAsync(IQueryable source, UserParams userParams) - { - return await CreateAsync(source, userParams.PageNumber, userParams.PageSize); - } - - public static async Task> CreateAsync(IQueryable source, int pageNumber, int pageSize) - { - // NOTE: OrderBy warning being thrown here even if query has the orderby statement - var countTask = source.CountAsync(); - var itemsTask = source.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToListAsync(); - - await Task.WhenAll(countTask, itemsTask); - - return new PagedList(itemsTask.Result, countTask.Result, pageNumber, pageSize); - } - - public static PagedList Create(IEnumerable items, int totalCount, int pageNumber, int pageSize) - { - return new PagedList(items, totalCount, pageNumber, pageSize); - } -} diff --git a/API/Helpers/ParserInfoHelpers.cs b/API/Helpers/ParserInfoHelpers.cs deleted file mode 100644 index fc8d7227a..000000000 --- a/API/Helpers/ParserInfoHelpers.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Collections.Generic; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Services.Tasks.Scanner; -using API.Services.Tasks.Scanner.Parser; - -namespace API.Helpers; -#nullable enable - -public static class ParserInfoHelpers -{ - /// - /// Checks each parser info to see if there is a name match and if so, checks if the format matches the Series object. - /// This accounts for if the Series has an Unknown type and if so, considers it matching. - /// - /// - /// - /// - public static bool SeriesHasMatchingParserInfoFormat(Series series, - Dictionary> parsedSeries) - { - var format = MangaFormat.Unknown; - foreach (var pSeries in parsedSeries.Keys) - { - var name = pSeries.Name; - var normalizedName = name.ToNormalized(); - - if (normalizedName == series.NormalizedName || - normalizedName == series.Name.ToNormalized() || - name == series.Name || name == series.LocalizedName || - name == series.OriginalName || - normalizedName == series.OriginalName?.ToNormalized()) - { - format = pSeries.Format; - if (format == series.Format) - { - return true; - } - } - } - - if (series.Format == MangaFormat.Unknown) - { - return true; - } - - return format == series.Format; - } -} diff --git a/API/Properties/launchSettings.json b/API/Properties/launchSettings.json deleted file mode 100644 index 677d81685..000000000 --- a/API/Properties/launchSettings.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:14778", - "sslPort": 44368 - } - }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "swagger", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "API": { - "commandName": "Project", - "dotnetRunMessages": "true", - "launchBrowser": false, - "launchUrl": "swagger", - "applicationUrl": "https://localhost:5001;http://localhost:5000", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/API/Services/AccountService.cs b/API/Services/AccountService.cs deleted file mode 100644 index 6bffb864c..000000000 --- a/API/Services/AccountService.cs +++ /dev/null @@ -1,335 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.Entities; -using API.Entities.Enums; -using API.Errors; -using API.Extensions; -using API.Helpers.Builders; -using AutoMapper; -using Kavita.Common; -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Services; - -#nullable enable - -public interface IAccountService -{ - Task> ChangeUserPassword(AppUser user, string newPassword); - Task> ValidatePassword(AppUser user, string password); - Task> ValidateUsername(string? username); - Task> ValidateEmail(string email); - Task HasBookmarkPermission(AppUser? user); - Task HasDownloadPermission(AppUser? user); - Task CanChangeAgeRestriction(AppUser? user); - - /// - /// - /// - /// The user who is changing the identity - /// the user being changed - /// the provider being changed to - /// If true, user should not be updated by kavita (anymore) - /// Throws if invalid actions are being performed - Task ChangeIdentityProvider(int actingUserId, AppUser user, IdentityProvider identityProvider); - /// - /// Removes access to all libraries, then grant access to all given libraries or all libraries if the user is admin. - /// Creates side nav streams as well - /// - /// - /// - /// - /// - /// Ensure that the users SideNavStreams are loaded - /// Does NOT commit - Task UpdateLibrariesForUser(AppUser user, IList librariesIds, bool hasAdminRole); - Task> UpdateRolesForUser(AppUser user, IList roles); - /// - /// Seeds all information necessary for a new user - /// - /// - /// - Task SeedUser(AppUser user); - void AddDefaultStreamsToUser(AppUser user); - Task AddDefaultReadingProfileToUser(AppUser user); -} - -public partial class AccountService : IAccountService -{ - private readonly ILocalizationService _localizationService; - private readonly UserManager _userManager; - private readonly ILogger _logger; - private readonly IUnitOfWork _unitOfWork; - private readonly IMapper _mapper; - public const string DefaultPassword = "[k.2@RZ!mxCQkJzE"; - public static readonly Regex AllowedUsernameRegex = AllowedUsernameRegexAttr(); - - - public AccountService(UserManager userManager, ILogger logger, IUnitOfWork unitOfWork, - IMapper mapper, ILocalizationService localizationService) - { - _localizationService = localizationService; - _userManager = userManager; - _logger = logger; - _unitOfWork = unitOfWork; - _mapper = mapper; - } - - public async Task> ChangeUserPassword(AppUser user, string newPassword) - { - var passwordValidationIssues = (await ValidatePassword(user, newPassword)).ToList(); - if (passwordValidationIssues.Count != 0) return passwordValidationIssues; - - var result = await _userManager.RemovePasswordAsync(user); - if (!result.Succeeded) - { - _logger.LogError("Could not update password"); - return result.Errors.Select(e => new ApiException(400, e.Code, e.Description)); - } - - result = await _userManager.AddPasswordAsync(user, newPassword); - if (result.Succeeded) return []; - - _logger.LogError("Could not update password"); - return result.Errors.Select(e => new ApiException(400, e.Code, e.Description)); - } - - public async Task> ValidatePassword(AppUser user, string password) - { - foreach (var validator in _userManager.PasswordValidators) - { - var validationResult = await validator.ValidateAsync(_userManager, user, password); - if (!validationResult.Succeeded) - { - return validationResult.Errors.Select(e => new ApiException(400, e.Code, e.Description)); - } - } - - return Array.Empty(); - } - public async Task> ValidateUsername(string? username) - { - if (string.IsNullOrWhiteSpace(username) || !AllowedUsernameRegex.IsMatch(username)) - { - return [new ApiException(400, "Invalid username")]; - } - - // Reverted because of https://go.microsoft.com/fwlink/?linkid=2129535 - if (await _userManager.Users.AnyAsync(x => x.NormalizedUserName != null - && x.NormalizedUserName == username.ToUpper())) - { - return - [ - new(400, "Username is already taken") - ]; - } - - return []; - } - - public async Task> ValidateEmail(string email) - { - var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(email); - if (user == null) return []; - - return - [ - new ApiException(400, "Email is already registered") - ]; - } - - /// - /// Does the user have the Bookmark permission or admin rights - /// - /// - /// - public async Task HasBookmarkPermission(AppUser? user) - { - if (user == null) return false; - var roles = await _userManager.GetRolesAsync(user); - - return roles.Contains(PolicyConstants.BookmarkRole) || roles.Contains(PolicyConstants.AdminRole); - } - - /// - /// Does the user have the Download permission or admin rights - /// - /// - /// - public async Task HasDownloadPermission(AppUser? user) - { - if (user == null) return false; - var roles = await _userManager.GetRolesAsync(user); - - return roles.Contains(PolicyConstants.DownloadRole) || roles.Contains(PolicyConstants.AdminRole); - } - - /// - /// Does the user have Change Restriction permission or admin rights and not Read Only - /// - /// - /// - 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.ChangeRestrictionRole) || roles.Contains(PolicyConstants.AdminRole); - } - - public async Task ChangeIdentityProvider(int actingUserId, AppUser user, IdentityProvider identityProvider) - { - var defaultAdminUser = await _unitOfWork.UserRepository.GetDefaultAdminUser(); - if (user.Id == defaultAdminUser.Id) - { - if (identityProvider == IdentityProvider.OpenIdConnect) - { - throw new KavitaException(await _localizationService.Translate(actingUserId, "cannot-change-identity-provider-original-user")); - } - - return false; - } - - // Allow changes if users aren't being synced - var oidcSettings = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; - if (!oidcSettings.SyncUserSettings) - { - user.IdentityProvider = identityProvider; - await _unitOfWork.CommitAsync(); - return false; - } - - // Don't allow changes to the user if they're managed by oidc, and their identity provider isn't being changed to something else - if (user.IdentityProvider == IdentityProvider.OpenIdConnect && identityProvider == IdentityProvider.OpenIdConnect) - { - throw new KavitaException(await _localizationService.Translate(actingUserId, "oidc-managed")); - } - - user.IdentityProvider = identityProvider; - await _unitOfWork.CommitAsync(); - return user.IdentityProvider == IdentityProvider.OpenIdConnect; - } - - public async Task UpdateLibrariesForUser(AppUser user, IList librariesIds, bool hasAdminRole) - { - var allLibraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync(LibraryIncludes.AppUser)).ToList(); - var currentLibrary = allLibraries.Where(l => l.AppUsers.Contains(user)).ToList(); - - List libraries; - if (hasAdminRole) - { - _logger.LogDebug("{UserId} is admin. Granting access to all libraries", user.Id); - libraries = allLibraries; - } - else - { - libraries = allLibraries.Where(lib => librariesIds.Contains(lib.Id)).ToList(); - } - - var toRemove = currentLibrary.Except(libraries); - var toAdd = libraries.Except(currentLibrary); - - foreach (var lib in toRemove) - { - lib.AppUsers ??= []; - lib.AppUsers.Remove(user); - user.RemoveSideNavFromLibrary(lib); - } - - foreach (var lib in toAdd) - { - lib.AppUsers ??= []; - lib.AppUsers.Add(user); - user.CreateSideNavFromLibrary(lib); - } - } - - public async Task> UpdateRolesForUser(AppUser user, IList roles) - { - var existingRoles = await _userManager.GetRolesAsync(user); - var hasAdminRole = roles.Contains(PolicyConstants.AdminRole); - if (!hasAdminRole) - { - roles.Add(PolicyConstants.PlebRole); - } - - if (existingRoles.Except(roles).Any() || roles.Except(existingRoles).Any()) - { - var roleResult = await _userManager.RemoveFromRolesAsync(user, existingRoles); - if (!roleResult.Succeeded) return roleResult.Errors; - - roleResult = await _userManager.AddToRolesAsync(user, roles); - if (!roleResult.Succeeded) return roleResult.Errors; - } - - return []; - } - - public async Task SeedUser(AppUser user) - { - AddDefaultStreamsToUser(user); - AddDefaultHighlightSlotsToUser(user); - AddAuthKeys(user); - await AddDefaultReadingProfileToUser(user); // Commits - } - - /// - /// Assign default streams - /// - /// - public void AddDefaultStreamsToUser(AppUser user) - { - foreach (var newStream in Seed.DefaultStreams.Select(_mapper.Map)) - { - user.DashboardStreams.Add(newStream); - } - - foreach (var stream in Seed.DefaultSideNavStreams.Select(_mapper.Map)) - { - user.SideNavStreams.Add(stream); - } - } - - private void AddDefaultHighlightSlotsToUser(AppUser user) - { - if (user.UserPreferences.BookReaderHighlightSlots.Any()) return; - - user.UserPreferences.BookReaderHighlightSlots = Seed.DefaultHighlightSlots.ToList(); - _unitOfWork.UserRepository.Update(user); - } - - private void AddAuthKeys(AppUser user) - { - if (user.AuthKeys.Any()) return; - - user.AuthKeys = Seed.CreateDefaultAuthKeys(); - _unitOfWork.UserRepository.Update(user); - } - - /// - /// Assign default reading profile - /// - /// - public async Task AddDefaultReadingProfileToUser(AppUser user) - { - var profile = new AppUserReadingProfileBuilder(user.Id) - .WithName("Default Profile") - .WithKind(ReadingProfileKind.Default) - .Build(); - _unitOfWork.AppUserReadingProfileRepository.Add(profile); - await _unitOfWork.CommitAsync(); - } - - [GeneratedRegex(@"^[a-zA-Z0-9\-._@+/]*$")] - private static partial Regex AllowedUsernameRegexAttr(); -} diff --git a/API/Services/AuthKeyService.cs b/API/Services/AuthKeyService.cs deleted file mode 100644 index 4323738da..000000000 --- a/API/Services/AuthKeyService.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Services; - -public interface IAuthKeyService -{ - Task UpdateLastAccessedAsync(string authKey); -} - -public class AuthKeyService(DataContext context, ILogger logger) : IAuthKeyService -{ - public async Task UpdateLastAccessedAsync(string authKey) - { - logger.LogTrace("Updating last accessed Auth key: {AuthKey}", authKey); - await context.AppUserAuthKey - .Where(k => k.Key == authKey) - .ExecuteUpdateAsync(s => s.SetProperty(k => k.LastAccessedAtUtc, DateTime.UtcNow)); - } -} diff --git a/API/Services/Caching/AuthKeyCacheInvalidator.cs b/API/Services/Caching/AuthKeyCacheInvalidator.cs deleted file mode 100644 index 45870b4cc..000000000 --- a/API/Services/Caching/AuthKeyCacheInvalidator.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using API.Middleware; -using Microsoft.Extensions.Caching.Hybrid; - -namespace API.Services.Caching; - -public interface IAuthKeyCacheInvalidator -{ - /// - /// Invalidates the cached authentication data for a specific auth key. - /// Call this when a key is rotated or deleted. - /// - /// The actual key value (not the ID) - /// Cancellation token - Task InvalidateAsync(string keyValue, CancellationToken cancellationToken = default); -} - -public class AuthKeyCacheInvalidator(HybridCache cache) : IAuthKeyCacheInvalidator -{ - public async Task InvalidateAsync(string keyValue, CancellationToken cancellationToken = default) - { - var cacheKey = AuthKeyAuthenticationHandler.CreateCacheKey(keyValue); - await cache.RemoveAsync(cacheKey, cancellationToken); - } -} diff --git a/API/Services/CollectionTagService.cs b/API/Services/CollectionTagService.cs deleted file mode 100644 index a598c1a47..000000000 --- a/API/Services/CollectionTagService.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.Collection; -using API.Entities; -using API.Extensions; -using API.Services.Plus; -using API.SignalR; -using Kavita.Common; - -namespace API.Services; -#nullable enable - -public interface ICollectionTagService -{ - Task DeleteTag(int tagId, AppUser user); - Task UpdateTag(AppUserCollectionDto dto, int userId); - Task RemoveTagFromSeries(AppUserCollection? tag, IEnumerable seriesIds); -} - - -public class CollectionTagService : ICollectionTagService -{ - private readonly IUnitOfWork _unitOfWork; - private readonly IEventHub _eventHub; - - public CollectionTagService(IUnitOfWork unitOfWork, IEventHub eventHub) - { - _unitOfWork = unitOfWork; - _eventHub = eventHub; - } - - public async Task DeleteTag(int tagId, AppUser user) - { - var collectionTag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(tagId); - if (collectionTag == null) return true; - - user.Collections.Remove(collectionTag); - - if (!_unitOfWork.HasChanges()) return true; - - return await _unitOfWork.CommitAsync(); - } - - - public async Task UpdateTag(AppUserCollectionDto dto, int userId) - { - var existingTag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(dto.Id); - if (existingTag == null) throw new KavitaException("collection-doesnt-exist"); - if (existingTag.AppUserId != userId) throw new KavitaException("access-denied"); - - var title = dto.Title.Trim(); - if (string.IsNullOrEmpty(title)) throw new KavitaException("collection-tag-title-required"); - - // Ensure the title doesn't exist on the user's account already - if (!title.Equals(existingTag.Title) && await _unitOfWork.CollectionTagRepository.CollectionExists(dto.Title, userId)) - throw new KavitaException("collection-tag-duplicate"); - - existingTag.Items ??= []; - if (existingTag.Source == ScrobbleProvider.Kavita) - { - existingTag.Title = title; - existingTag.NormalizedTitle = dto.Title.ToNormalized(); - } - - var roles = await _unitOfWork.UserRepository.GetRoles(userId); - if (roles.Contains(PolicyConstants.AdminRole) || roles.Contains(PolicyConstants.PromoteRole)) - { - existingTag.Promoted = dto.Promoted; - } - existingTag.CoverImageLocked = dto.CoverImageLocked; - _unitOfWork.CollectionTagRepository.Update(existingTag); - - // Check if Tag has updated (Summary) - var summary = (dto.Summary ?? string.Empty).Trim(); - if (existingTag.Summary == null || !existingTag.Summary.Equals(summary)) - { - existingTag.Summary = summary; - _unitOfWork.CollectionTagRepository.Update(existingTag); - } - - // If we unlock the cover image it means reset - if (!dto.CoverImageLocked) - { - existingTag.CoverImageLocked = false; - existingTag.CoverImage = string.Empty; - await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, - MessageFactory.CoverUpdateEvent(existingTag.Id, MessageFactoryEntityTypes.Collection), false); - _unitOfWork.CollectionTagRepository.Update(existingTag); - } - - if (!_unitOfWork.HasChanges()) return true; - return await _unitOfWork.CommitAsync(); - } - - /// - /// Removes series from Collection tag. Will recalculate max age rating. - /// - /// - /// - /// - public async Task RemoveTagFromSeries(AppUserCollection? tag, IEnumerable seriesIds) - { - if (tag == null) return false; - - tag.Items ??= []; - tag.Items = tag.Items.Where(s => !seriesIds.Contains(s.Id)).ToList(); - - if (tag.Items.Count == 0) - { - _unitOfWork.CollectionTagRepository.Remove(tag); - } - - if (!_unitOfWork.HasChanges()) return true; - - var result = await _unitOfWork.CommitAsync(); - if (tag.Items.Count > 0) - { - await _unitOfWork.CollectionTagRepository.UpdateCollectionAgeRating(tag); - } - - return result; - } -} diff --git a/API/Services/DeviceService.cs b/API/Services/DeviceService.cs deleted file mode 100644 index 39bc3d890..000000000 --- a/API/Services/DeviceService.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.DTOs.Device; -using API.DTOs.Device.EmailDevice; -using API.DTOs.Email; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Enums.Device; -using API.Helpers.Builders; -using Kavita.Common; -using Microsoft.Extensions.Logging; - -namespace API.Services; -#nullable enable - -public interface IDeviceService -{ - Task Create(CreateEmailDeviceDto dto, AppUser userWithDevices); - Task Update(UpdateEmailDeviceDto dto, AppUser userWithDevices); - Task Delete(AppUser userWithDevices, int deviceId); - Task SendTo(IReadOnlyList chapterIds, int deviceId); -} - -public class DeviceService : IDeviceService -{ - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - private readonly IEmailService _emailService; - private readonly IReadingProfileService _readingProfileService; - - public DeviceService(IUnitOfWork unitOfWork, ILogger logger, IEmailService emailService, IReadingProfileService readingProfileService) - { - _unitOfWork = unitOfWork; - _logger = logger; - _emailService = emailService; - _readingProfileService = readingProfileService; - } - - public async Task Create(CreateEmailDeviceDto dto, AppUser userWithDevices) - { - try - { - userWithDevices.Devices ??= new List(); - var existingDevice = userWithDevices.Devices.SingleOrDefault(d => d.Name!.Equals(dto.Name)); - if (existingDevice != null) throw new KavitaException("device-duplicate"); - - existingDevice = new DeviceBuilder(dto.Name) - .WithPlatform(dto.Platform) - .WithEmail(dto.EmailAddress) - .Build(); - - - userWithDevices.Devices.Add(existingDevice); - _unitOfWork.UserRepository.Update(userWithDevices); - - if (!_unitOfWork.HasChanges()) return existingDevice; - if (await _unitOfWork.CommitAsync()) return existingDevice; - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an error when creating your device"); - await _unitOfWork.RollbackAsync(); - } - - return null; - } - - public async Task Update(UpdateEmailDeviceDto dto, AppUser userWithDevices) - { - try - { - var existingDevice = userWithDevices.Devices.SingleOrDefault(d => d.Id == dto.Id); - if (existingDevice == null) throw new KavitaException("device-not-created"); - - existingDevice.Name = dto.Name; - existingDevice.Platform = dto.Platform; - existingDevice.EmailAddress = dto.EmailAddress; - - if (!_unitOfWork.HasChanges()) return existingDevice; - if (await _unitOfWork.CommitAsync()) return existingDevice; - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an error when updating your device"); - await _unitOfWork.RollbackAsync(); - } - - return null; - } - - public async Task Delete(AppUser userWithDevices, int deviceId) - { - try - { - userWithDevices.Devices = userWithDevices.Devices.Where(d => d.Id != deviceId).ToList(); - _unitOfWork.UserRepository.Update(userWithDevices); - - await _readingProfileService.RemoveDeviceLinks(userWithDevices.Id, deviceId); - - if (!_unitOfWork.HasChanges()) return true; - if (await _unitOfWork.CommitAsync()) return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an issue with deleting the device, {DeviceId} for user {UserName}", deviceId, userWithDevices.UserName); - } - - return false; - } - - public async Task SendTo(IReadOnlyList chapterIds, int deviceId) - { - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - if (!settings.IsEmailSetupForSendToDevice()) - throw new KavitaException("send-to-kavita-email"); - - var device = await _unitOfWork.DeviceRepository.GetDeviceById(deviceId); - if (device == null) throw new KavitaException("device-doesnt-exist"); - - var files = await _unitOfWork.ChapterRepository.GetFilesForChaptersAsync(chapterIds); - if (files.Any(f => f.Format is not (MangaFormat.Epub or MangaFormat.Pdf)) && device.Platform == EmailDevicePlatform.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"); - - - try - { - device.UpdateLastUsed(); - _unitOfWork.DeviceRepository.Update(device); - await _unitOfWork.CommitAsync(); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an issue updating device last used time"); - } - - var success = await _emailService.SendFilesToEmail(new SendToDto() - { - DestinationEmail = device.EmailAddress!, - FilePaths = files.Select(m => m.FilePath) - }); - - return success; - } -} diff --git a/API/Services/KoreaderService.cs b/API/Services/KoreaderService.cs deleted file mode 100644 index 4c5f82552..000000000 --- a/API/Services/KoreaderService.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System.Threading.Tasks; -using API.Data; -using API.DTOs.Koreader; -using API.DTOs.Progress; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers; -using API.Helpers.Builders; -using API.Services.Reading; -using Kavita.Common; -using Microsoft.Extensions.Logging; - -namespace API.Services; - -#nullable enable - -public interface IKoreaderService -{ - Task SaveProgress(KoreaderBookDto koreaderBookDto, int userId); - Task GetProgress(string bookHash, int userId); -} - -public class KoreaderService : IKoreaderService -{ - private readonly IReaderService _readerService; - private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; - private readonly ILogger _logger; - - public KoreaderService(IReaderService readerService, IUnitOfWork unitOfWork, ILocalizationService localizationService, ILogger logger) - { - _readerService = readerService; - _unitOfWork = unitOfWork; - _localizationService = localizationService; - _logger = logger; - } - - /// - /// Given a Koreader hash, locate the underlying file and generate/update a progress event. - /// - /// - /// - public async Task SaveProgress(KoreaderBookDto koreaderBookDto, int userId) - { - _logger.LogDebug("Saving Koreader progress for User ({UserId}): {KoreaderProgress}", userId, koreaderBookDto.progress.Sanitize()); - var file = await _unitOfWork.MangaFileRepository.GetByKoreaderHash(koreaderBookDto.document); - if (file == null) throw new KavitaException(await _localizationService.Translate(userId, "file-missing")); - - var userProgressDto = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(file.ChapterId, userId); - if (userProgressDto == null) - { - var chapterDto = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(file.ChapterId, userId); - if (chapterDto == null) throw new KavitaException(await _localizationService.Translate(userId, "chapter-doesnt-exist")); - - var volumeDto = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapterDto.VolumeId); - if (volumeDto == null) throw new KavitaException(await _localizationService.Translate(userId, "volume-doesnt-exist")); - - var seriesDto = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(volumeDto.SeriesId, userId); - if (seriesDto == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist")); - - userProgressDto = new ProgressDto() - { - PageNum = 0, // This is updated in KoreaderHelper.UpdateProgressDto - ChapterId = file.ChapterId, - VolumeId = chapterDto.VolumeId, - SeriesId = seriesDto.Id, - LibraryId = seriesDto.LibraryId - }; - } - - // Update the bookScrollId if possible - var reportedProgress = koreaderBookDto.progress; - KoreaderHelper.UpdateProgressDto(userProgressDto, koreaderBookDto.progress); - - _logger.LogDebug("Converted KOReader progress from {ProgressEncoding} to Page {PageNum} with ScrollId: {ScrollId}", reportedProgress.Sanitize(), - userProgressDto.PageNum, userProgressDto.BookScrollId?.Sanitize() ?? string.Empty); - - // Normal saving from kavita will be //body/h2[1] - await _readerService.SaveReadingProgress(userProgressDto, userId); - } - - /// - /// Returns a Koreader Dto representing current book and the progress within - /// - /// - /// - /// - public async Task GetProgress(string bookHash, int userId) - { - var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - - var file = await _unitOfWork.MangaFileRepository.GetByKoreaderHash(bookHash); - if (file == null) throw new KavitaException(await _localizationService.Translate(userId, "file-missing")); - - var progressDto = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(file.ChapterId, userId); - - // Non-epubs use the pageNum as the progress. KOReader is 1-index based - var koreaderProgress = $"{progressDto?.PageNum + 1 ?? 0}"; - if (file.Format == MangaFormat.Epub) - { - koreaderProgress = KoreaderHelper.GetKoreaderPosition(progressDto); - } - - var response = new KoreaderBookDtoBuilder(bookHash) - .WithProgress(koreaderProgress) - .WithPercentage(progressDto?.PageNum, file.Pages) - .WithDeviceId(settingsDto.InstallId, userId) - .WithTimestamp(progressDto?.LastModifiedUtc) - .Build(); - - _logger.LogDebug("Responding to KOReader with Page {PageNum}, Scroll Id: {ScrollId}, and Progress: {Progress}", - progressDto?.PageNum, response.progress.Sanitize(), response.percentage); - - - return response; - } -} diff --git a/API/Services/MediaConversionService.cs b/API/Services/MediaConversionService.cs deleted file mode 100644 index 4220b065e..000000000 --- a/API/Services/MediaConversionService.cs +++ /dev/null @@ -1,326 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using API.Comparators; -using API.Data; -using API.Entities.Enums; -using API.Extensions; -using API.SignalR; -using Hangfire; -using Microsoft.Extensions.Logging; - -namespace API.Services; - -public interface IMediaConversionService -{ - [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)] - Task ConvertAllBookmarkToEncoding(); - [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)] - Task ConvertAllCoversToEncoding(); - [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)] - Task ConvertAllManagedMediaToEncodingFormat(); - - Task SaveAsEncodingFormat(string imageDirectory, string filename, string targetFolder, - EncodeFormat encodeFormat); -} - -public class MediaConversionService : IMediaConversionService -{ - public const string Name = "MediaConversionService"; - public static readonly string[] ConversionMethods = ["ConvertAllBookmarkToEncoding", "ConvertAllCoversToEncoding", "ConvertAllManagedMediaToEncodingFormat"]; - private readonly IUnitOfWork _unitOfWork; - private readonly IImageService _imageService; - private readonly IEventHub _eventHub; - private readonly IDirectoryService _directoryService; - private readonly ILogger _logger; - - public MediaConversionService(IUnitOfWork unitOfWork, IImageService imageService, IEventHub eventHub, - IDirectoryService directoryService, ILogger logger) - { - _unitOfWork = unitOfWork; - _imageService = imageService; - _eventHub = eventHub; - _directoryService = directoryService; - _logger = logger; - } - - /// - /// Converts all Kavita managed media (bookmarks, covers, favicons, etc) to the saved target encoding. - /// Do not invoke anyway except via Hangfire. - /// - /// This is a long-running job - /// - [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)] - public async Task ConvertAllManagedMediaToEncodingFormat() - { - await ConvertAllBookmarkToEncoding(); - await ConvertAllCoversToEncoding(); - await CoverAllFaviconsToEncoding(); - - } - - /// - /// This is a long-running job that will convert all bookmarks into a format that is not PNG. Do not invoke anyway except via Hangfire. - /// - [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)] - public async Task ConvertAllBookmarkToEncoding() - { - var bookmarkDirectory = - (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; - var encodeFormat = - (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; - - if (encodeFormat == EncodeFormat.PNG) - { - _logger.LogError("Cannot convert media to PNG"); - return; - } - - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertBookmarksProgressEvent(0F, ProgressEventType.Started)); - var bookmarks = (await _unitOfWork.UserRepository.GetAllBookmarksAsync()) - .Where(b => !b.FileName.EndsWith(encodeFormat.GetExtension())).ToList(); - - var count = 1F; - foreach (var bookmark in bookmarks) - { - bookmark.FileName = await SaveAsEncodingFormat(bookmarkDirectory, bookmark.FileName, - BookmarkService.BookmarkStem(bookmark.AppUserId, bookmark.SeriesId, bookmark.ChapterId), encodeFormat); - _unitOfWork.UserRepository.Update(bookmark); - await _unitOfWork.CommitAsync(); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertBookmarksProgressEvent(count / bookmarks.Count, ProgressEventType.Updated)); - count++; - } - - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertBookmarksProgressEvent(1F, ProgressEventType.Ended)); - - _logger.LogInformation("[MediaConversionService] Converted bookmarks to {Format}", encodeFormat); - } - - /// - /// This is a long-running job that will convert all covers into WebP. Do not invoke anyway except via Hangfire. - /// - [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)] - public async Task ConvertAllCoversToEncoding() - { - var coverDirectory = _directoryService.CoverImageDirectory; - var encodeFormat = - (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; - - if (encodeFormat == EncodeFormat.PNG) - { - _logger.LogError("Cannot convert media to PNG"); - return; - } - - _logger.LogInformation("[MediaConversionService] Starting conversion of all covers to {Format}", encodeFormat); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertCoverProgressEvent(0F, ProgressEventType.Started)); - - var chapterCovers = await _unitOfWork.ChapterRepository.GetAllChaptersWithCoversInDifferentEncoding(encodeFormat); - var customSeriesCovers = await _unitOfWork.SeriesRepository.GetAllWithCoversInDifferentEncoding(encodeFormat); - var seriesCovers = await _unitOfWork.SeriesRepository.GetAllWithCoversInDifferentEncoding(encodeFormat, false); - var nonCustomOrConvertedVolumeCovers = await _unitOfWork.VolumeRepository.GetAllWithCoversInDifferentEncoding(encodeFormat); - - var readingListCovers = await _unitOfWork.ReadingListRepository.GetAllWithCoversInDifferentEncoding(encodeFormat); - var libraryCovers = await _unitOfWork.LibraryRepository.GetAllWithCoversInDifferentEncoding(encodeFormat); - var collectionCovers = await _unitOfWork.CollectionTagRepository.GetAllWithCoversInDifferentEncoding(encodeFormat); - - var totalCount = chapterCovers.Count + seriesCovers.Count + readingListCovers.Count + - libraryCovers.Count + collectionCovers.Count + nonCustomOrConvertedVolumeCovers.Count + customSeriesCovers.Count; - - var count = 1F; - _logger.LogInformation("[MediaConversionService] Starting conversion of chapters"); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertCoverProgressEvent(0, ProgressEventType.Started)); - _logger.LogInformation("[MediaConversionService] Starting conversion of libraries"); - foreach (var library in libraryCovers) - { - if (string.IsNullOrEmpty(library.CoverImage)) continue; - - var newFile = await SaveAsEncodingFormat(coverDirectory, library.CoverImage, coverDirectory, encodeFormat); - library.CoverImage = Path.GetFileName(newFile); - _unitOfWork.LibraryRepository.Update(library); - await _unitOfWork.CommitAsync(); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated)); - count++; - } - - _logger.LogInformation("[MediaConversionService] Starting conversion of reading lists"); - foreach (var readingList in readingListCovers) - { - if (string.IsNullOrEmpty(readingList.CoverImage)) continue; - - var newFile = await SaveAsEncodingFormat(coverDirectory, readingList.CoverImage, coverDirectory, encodeFormat); - readingList.CoverImage = Path.GetFileName(newFile); - _unitOfWork.ReadingListRepository.Update(readingList); - await _unitOfWork.CommitAsync(); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated)); - count++; - } - - _logger.LogInformation("[MediaConversionService] Starting conversion of collections"); - foreach (var collection in collectionCovers) - { - if (string.IsNullOrEmpty(collection.CoverImage)) continue; - - var newFile = await SaveAsEncodingFormat(coverDirectory, collection.CoverImage, coverDirectory, encodeFormat); - collection.CoverImage = Path.GetFileName(newFile); - _unitOfWork.CollectionTagRepository.Update(collection); - await _unitOfWork.CommitAsync(); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated)); - count++; - } - - _logger.LogInformation("[MediaConversionService] Starting conversion of chapters"); - foreach (var chapter in chapterCovers) - { - if (string.IsNullOrEmpty(chapter.CoverImage)) continue; - - var newFile = await SaveAsEncodingFormat(coverDirectory, chapter.CoverImage, coverDirectory, encodeFormat); - chapter.CoverImage = Path.GetFileName(newFile); - _unitOfWork.ChapterRepository.Update(chapter); - await _unitOfWork.CommitAsync(); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated)); - count++; - } - - // Now null out all series and volumes that aren't webp or custom - _logger.LogInformation("[MediaConversionService] Starting conversion of volumes"); - foreach (var volume in nonCustomOrConvertedVolumeCovers) - { - if (string.IsNullOrEmpty(volume.CoverImage)) continue; - volume.CoverImage = volume.Chapters.MinBy(x => x.MinNumber, ChapterSortComparerDefaultFirst.Default)?.CoverImage; - _unitOfWork.VolumeRepository.Update(volume); - await _unitOfWork.CommitAsync(); - } - - _logger.LogInformation("[MediaConversionService] Starting conversion of series"); - foreach (var series in customSeriesCovers) - { - if (string.IsNullOrEmpty(series.CoverImage)) continue; - - var newFile = await SaveAsEncodingFormat(coverDirectory, series.CoverImage, coverDirectory, encodeFormat); - series.CoverImage = string.IsNullOrEmpty(newFile) ? - series.CoverImage.Replace(Path.GetExtension(series.CoverImage), encodeFormat.GetExtension()) : Path.GetFileName(newFile); - - _unitOfWork.SeriesRepository.Update(series); - await _unitOfWork.CommitAsync(); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated)); - count++; - } - - foreach (var series in seriesCovers) - { - if (string.IsNullOrEmpty(series.CoverImage)) continue; - series.CoverImage = series.GetCoverImage(); - if (series.CoverImage == null) - { - _logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null: {SeriesId}", series.Id); - } - _unitOfWork.SeriesRepository.Update(series); - await _unitOfWork.CommitAsync(); - } - - // Get all volumes and remap their covers - - // Get all series and remap their covers - - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertCoverProgressEvent(1F, ProgressEventType.Ended)); - - _logger.LogInformation("[MediaConversionService] Converted covers to {Format}", encodeFormat); - } - - private async Task CoverAllFaviconsToEncoding() - { - var encodeFormat = - (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; - - if (encodeFormat == EncodeFormat.PNG) - { - _logger.LogError("Cannot convert media to PNG"); - return; - } - - _logger.LogInformation("[MediaConversionService] Starting conversion of favicons to {Format}", encodeFormat); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertBookmarksProgressEvent(0F, ProgressEventType.Started)); - var pngFavicons = _directoryService.GetFiles(_directoryService.FaviconDirectory) - .Where(b => !b.EndsWith(encodeFormat.GetExtension())). - ToList(); - - var count = 1F; - foreach (var file in pngFavicons) - { - await SaveAsEncodingFormat(_directoryService.FaviconDirectory, _directoryService.FileSystem.FileInfo.New(file).Name, _directoryService.FaviconDirectory, - encodeFormat); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertBookmarksProgressEvent(count / pngFavicons.Count, ProgressEventType.Updated)); - count++; - } - - - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ConvertBookmarksProgressEvent(1F, ProgressEventType.Ended)); - - _logger.LogInformation("[MediaConversionService] Converted favicons to {Format}", encodeFormat); - } - - - /// - /// Converts an image file, deletes original and returns the new path back - /// - /// Full Path to where files are stored - /// The file to convert - /// Full path to where files should be stored or any stem - /// Encoding Format - /// - public async Task SaveAsEncodingFormat(string imageDirectory, string filename, string targetFolder, EncodeFormat encodeFormat) - { - // This must be Public as it's used in via Hangfire as a background task - var fullSourcePath = _directoryService.FileSystem.Path.Join(imageDirectory, filename); - var fullTargetDirectory = fullSourcePath.Replace(new FileInfo(filename).Name, string.Empty); - - var newFilename = string.Empty; - _logger.LogDebug("Converting {Source} image into {Encoding} at {Target}", fullSourcePath, encodeFormat, fullTargetDirectory); - - if (!File.Exists(fullSourcePath)) - { - _logger.LogError("Requested to convert {File} but it doesn't exist", fullSourcePath); - return newFilename; - } - - try - { - // Convert target file to format then delete original target file - try - { - var targetFile = await _imageService.ConvertToEncodingFormat(fullSourcePath, fullTargetDirectory, encodeFormat); - var targetName = new FileInfo(targetFile).Name; - newFilename = Path.Join(targetFolder, targetName); - _directoryService.DeleteFiles(new[] {fullSourcePath}); - } - catch (Exception ex) - { - _logger.LogError(ex, "Could not convert image {FilePath} to {Format}", filename, encodeFormat); - newFilename = filename; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Could not convert image to {Format}", encodeFormat); - } - - return newFilename; - } - -} diff --git a/API/Services/MediaErrorService.cs b/API/Services/MediaErrorService.cs deleted file mode 100644 index 2c0a1df68..000000000 --- a/API/Services/MediaErrorService.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Threading.Tasks; -using API.Data; -using API.Helpers.Builders; -using Hangfire; - -namespace API.Services; - -public enum MediaErrorProducer -{ - BookService = 0, - ArchiveService = 1 - -} - -public interface IMediaErrorService -{ - void ReportMediaIssue(string filename, MediaErrorProducer producer, string errorMessage, string details); - void ReportMediaIssue(string filename, MediaErrorProducer producer, string errorMessage, Exception ex); - Task ReportMediaIssueAsync(string filename, MediaErrorProducer producer, string errorMessage, string details); - Task ReportMediaIssueAsync(string filename, MediaErrorProducer producer, string errorMessage, Exception ex); -} - -public class MediaErrorService : IMediaErrorService -{ - private readonly IUnitOfWork _unitOfWork; - - public MediaErrorService(IUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } - - - - public void ReportMediaIssue(string filename, MediaErrorProducer producer, string errorMessage, Exception ex) - { - // TODO: Localize all these messages - // To avoid overhead on commits, do async. We don't need to wait. - BackgroundJob.Enqueue(() => ReportMediaIssueAsync(filename, producer, errorMessage, ex.Message)); - } - - public void ReportMediaIssue(string filename, MediaErrorProducer producer, string errorMessage, string details) - { - // To avoid overhead on commits, do async. We don't need to wait. - BackgroundJob.Enqueue(() => ReportMediaIssueAsync(filename, producer, errorMessage, details)); - } - - public async Task ReportMediaIssueAsync(string filename, MediaErrorProducer producer, string errorMessage, Exception ex) - { - await ReportMediaIssueAsync(filename, producer, errorMessage, ex.Message); - } - - public async Task ReportMediaIssueAsync(string filename, MediaErrorProducer producer, string errorMessage, string details) - { - var error = new MediaErrorBuilder(filename) - .WithComment(errorMessage) - .WithDetails(details) - .Build(); - - if (await _unitOfWork.MediaErrorRepository.ExistsAsync(error)) - { - return; - } - - - _unitOfWork.MediaErrorRepository.Attach(error); - await _unitOfWork.CommitAsync(); - } - -} diff --git a/API/Services/RatingService.cs b/API/Services/RatingService.cs deleted file mode 100644 index f0ec485bd..000000000 --- a/API/Services/RatingService.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using API.Data; -using API.DTOs; -using API.Entities; -using API.Entities.User; -using API.Services.Plus; -using Hangfire; -using Microsoft.Extensions.Logging; - -namespace API.Services; - -public interface IRatingService -{ - /// - /// Updates the users' rating for a given series - /// - /// Should include ratings - /// - /// - Task UpdateSeriesRating(AppUser user, UpdateRatingDto updateRatingDto); - - /// - /// Updates the users' rating for a given chapter - /// - /// Should include ratings - /// chapterId must be set - /// - Task UpdateChapterRating(AppUser user, UpdateRatingDto updateRatingDto); -} - -public class RatingService: IRatingService -{ - - private readonly IUnitOfWork _unitOfWork; - private readonly IScrobblingService _scrobblingService; - private readonly ILogger _logger; - - public RatingService(IUnitOfWork unitOfWork, IScrobblingService scrobblingService, ILogger logger) - { - _unitOfWork = unitOfWork; - _scrobblingService = scrobblingService; - _logger = logger; - } - - public async Task UpdateSeriesRating(AppUser user, UpdateRatingDto updateRatingDto) - { - var userRating = - await _unitOfWork.UserRepository.GetUserRatingAsync(updateRatingDto.SeriesId, user.Id) ?? - new AppUserRating(); - - try - { - userRating.Rating = Math.Clamp(updateRatingDto.UserRating, 0f, 5f); - userRating.HasBeenRated = true; - userRating.SeriesId = updateRatingDto.SeriesId; - - if (userRating.Id == 0) - { - user.Ratings ??= new List(); - user.Ratings.Add(userRating); - } - - _unitOfWork.UserRepository.Update(user); - - if (!_unitOfWork.HasChanges() || await _unitOfWork.CommitAsync()) - { - BackgroundJob.Enqueue(() => - _scrobblingService.ScrobbleRatingUpdate(user.Id, updateRatingDto.SeriesId, - userRating.Rating)); - return true; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an exception saving rating"); - } - - await _unitOfWork.RollbackAsync(); - user.Ratings?.Remove(userRating); - - return false; - } - - public async Task UpdateChapterRating(AppUser user, UpdateRatingDto updateRatingDto) - { - if (updateRatingDto.ChapterId == null) - { - return false; - } - - var userRating = - await _unitOfWork.UserRepository.GetUserChapterRatingAsync(user.Id, updateRatingDto.ChapterId.Value) ?? - new AppUserChapterRating(); - - try - { - userRating.Rating = Math.Clamp(updateRatingDto.UserRating, 0f, 5f); - userRating.HasBeenRated = true; - userRating.SeriesId = updateRatingDto.SeriesId; - userRating.ChapterId = updateRatingDto.ChapterId.Value; - - if (userRating.Id == 0) - { - user.ChapterRatings ??= new List(); - user.ChapterRatings.Add(userRating); - } - - _unitOfWork.UserRepository.Update(user); - - await _unitOfWork.CommitAsync(); - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an exception saving rating"); - } - - await _unitOfWork.RollbackAsync(); - user.ChapterRatings?.Remove(userRating); - - return false; - } - -} diff --git a/API/Services/Store/UserContext.cs b/API/Services/Store/UserContext.cs deleted file mode 100644 index 27650d063..000000000 --- a/API/Services/Store/UserContext.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using API.Entities.Progress; -using Kavita.Common; - -namespace API.Services.Store; -#nullable enable - -public interface IUserContext -{ - /// - /// Gets the current authenticated user's ID. - /// Returns null if user is not authenticated or on [AllowAnonymous] endpoint. - /// - int? GetUserId(); - - /// - /// Gets the current authenticated user's ID. - /// Throws KavitaException if user is not authenticated. - /// - int GetUserIdOrThrow(); - - /// - /// Gets the current authenticated user's username. - /// Returns null if user is not authenticated. - /// - /// Warning! Username's can contain .. and /, do not use folders or filenames explicitly with the Username - string? GetUsername(); - /// - /// The Roles associated with the Authenticated user - /// - IReadOnlyList Roles { get; } - /// - /// Returns true if the current user is authenticated. - /// - bool IsAuthenticated { get; } - /// - /// Gets the authentication method used (JWT, Auth Key, OIDC). - /// - AuthenticationType GetAuthenticationType(); - - - bool HasRole(string role); - bool HasAnyRole(params string[] roles); - bool HasAllRoles(params string[] roles); -} - -public class UserContext : IUserContext -{ - private int? _userId; - private string? _username; - private AuthenticationType _authType; - private List _roles = new(); - - public int? GetUserId() => _userId; - - public int GetUserIdOrThrow() - { - // TODO: Refactor this to use ProblemDetails and handle appropriately - return _userId ?? throw new KavitaException("User is not authenticated"); - } - - public string? GetUsername() => _username; - - public AuthenticationType GetAuthenticationType() => _authType; - - public bool IsAuthenticated { get; private set; } - public IReadOnlyList Roles => _roles.AsReadOnly(); - - // Internal method used by middleware to set context - internal void SetUserContext(int userId, string username, AuthenticationType authType, IEnumerable roles) - { - _userId = userId; - _username = username; - _authType = authType; - IsAuthenticated = true; - _roles = roles?.ToList() ?? []; - } - - internal void Clear() - { - _userId = null; - _username = null; - _authType = AuthenticationType.Unknown; - IsAuthenticated = false; - _roles.Clear(); - } - - public bool HasRole(string role) - { - return _roles.Any(r => r.Equals(role, StringComparison.OrdinalIgnoreCase)); - } - - public bool HasAnyRole(params string[] roles) - { - return roles.Any(HasRole); - } - - public bool HasAllRoles(params string[] roles) - { - return roles.All(HasRole); - } -} diff --git a/API/Services/StreamService.cs b/API/Services/StreamService.cs deleted file mode 100644 index 068143df6..000000000 --- a/API/Services/StreamService.cs +++ /dev/null @@ -1,435 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Dashboard; -using API.DTOs.SideNav; -using API.Entities; -using API.Entities.Enums; -using API.Helpers; -using API.SignalR; -using Kavita.Common; -using Kavita.Common.Helpers; -using Microsoft.Extensions.Logging; - -namespace API.Services; - -/// -/// For SideNavStream and DashboardStream manipulation -/// -public interface IStreamService -{ - Task> GetDashboardStreams(int userId, bool visibleOnly = true); - Task> GetSidenavStreams(int userId, bool visibleOnly = true); - Task> GetExternalSources(int userId); - Task CreateDashboardStreamFromSmartFilter(int userId, int smartFilterId); - Task UpdateDashboardStream(int userId, DashboardStreamDto dto); - Task UpdateDashboardStreamPosition(int userId, UpdateStreamPositionDto dto); - Task UpdateSideNavStreamBulk(int userId, BulkUpdateSideNavStreamVisibilityDto dto); - Task CreateSideNavStreamFromSmartFilter(int userId, int smartFilterId); - Task CreateSideNavStreamFromExternalSource(int userId, int externalSourceId); - Task UpdateSideNavStream(int userId, SideNavStreamDto dto); - Task UpdateSideNavStreamPosition(int userId, UpdateStreamPositionDto dto); - Task CreateExternalSource(int userId, ExternalSourceDto dto); - Task UpdateExternalSource(int userId, ExternalSourceDto dto); - Task DeleteExternalSource(int userId, int externalSourceId); - Task DeleteSideNavSmartFilterStream(int userId, int sideNavStreamId); - Task DeleteDashboardSmartFilterStream(int userId, int dashboardStreamId); - Task RenameSmartFilterStreams(AppUserSmartFilter smartFilter); -} - -public class StreamService : IStreamService -{ - private readonly IUnitOfWork _unitOfWork; - private readonly IEventHub _eventHub; - private readonly ILocalizationService _localizationService; - private readonly ILogger _logger; - - public StreamService(IUnitOfWork unitOfWork, IEventHub eventHub, ILocalizationService localizationService, ILogger logger) - { - _unitOfWork = unitOfWork; - _eventHub = eventHub; - _localizationService = localizationService; - _logger = logger; - } - - public async Task> GetDashboardStreams(int userId, bool visibleOnly = true) - { - return await _unitOfWork.UserRepository.GetDashboardStreams(userId, visibleOnly); - } - - public async Task> GetSidenavStreams(int userId, bool visibleOnly = true) - { - return await _unitOfWork.UserRepository.GetSideNavStreams(userId, visibleOnly); - } - - public async Task> GetExternalSources(int userId) - { - return await _unitOfWork.AppUserExternalSourceRepository.GetExternalSources(userId); - } - - public async Task CreateDashboardStreamFromSmartFilter(int userId, int smartFilterId) - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.DashboardStreams); - if (user == null) throw new KavitaException(await _localizationService.Translate(userId, "no-user")); - - var smartFilter = await _unitOfWork.AppUserSmartFilterRepository.GetById(smartFilterId); - if (smartFilter == null) throw new KavitaException(await _localizationService.Translate(userId, "smart-filter-doesnt-exist")); - - var stream = user.DashboardStreams.FirstOrDefault(d => d.SmartFilter?.Id == smartFilterId); - if (stream != null) throw new KavitaException(await _localizationService.Translate(userId, "smart-filter-already-in-use")); - - var maxOrder = user!.DashboardStreams.Max(d => d.Order); - var createdStream = new AppUserDashboardStream() - { - Name = smartFilter.Name, - IsProvided = false, - StreamType = DashboardStreamType.SmartFilter, - Visible = true, - Order = maxOrder + 1, - SmartFilter = smartFilter - }; - - user.DashboardStreams.Add(createdStream); - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); - - var ret = new DashboardStreamDto() - { - Id = createdStream.Id, - Name = createdStream.Name, - IsProvided = createdStream.IsProvided, - Visible = createdStream.Visible, - Order = createdStream.Order, - SmartFilterEncoded = smartFilter.Filter, - StreamType = createdStream.StreamType - }; - - await _eventHub.SendMessageToAsync(MessageFactory.DashboardUpdate, MessageFactory.DashboardUpdateEvent(user.Id), - userId); - - return ret; - } - - public async Task UpdateDashboardStream(int userId, DashboardStreamDto dto) - { - var stream = await _unitOfWork.UserRepository.GetDashboardStream(dto.Id); - if (stream == null) throw new KavitaException(await _localizationService.Translate(userId, "dashboard-stream-doesnt-exist")); - stream.Visible = dto.Visible; - - _unitOfWork.UserRepository.Update(stream); - await _unitOfWork.CommitAsync(); - await _eventHub.SendMessageToAsync(MessageFactory.DashboardUpdate, MessageFactory.DashboardUpdateEvent(userId), - userId); - } - - public async Task UpdateDashboardStreamPosition(int userId, UpdateStreamPositionDto dto) - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, - AppUserIncludes.DashboardStreams); - var stream = user?.DashboardStreams.FirstOrDefault(d => d.Id == dto.Id); - if (stream == null) - { - throw new KavitaException(await _localizationService.Translate(userId, "dashboard-stream-doesnt-exist")); - } - - if (stream.Order == dto.ToPosition) return; - - var list = user!.DashboardStreams.OrderBy(s => s.Order).ToList(); - OrderableHelper.ReorderItems(list, stream.Id, dto.ToPosition); - user.DashboardStreams = list; - - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); - if (!stream.Visible) return; - await _eventHub.SendMessageToAsync(MessageFactory.DashboardUpdate, MessageFactory.DashboardUpdateEvent(user.Id), - user.Id); - } - - public async Task UpdateSideNavStreamBulk(int userId, BulkUpdateSideNavStreamVisibilityDto dto) - { - var streams = await _unitOfWork.UserRepository.GetDashboardStreamsByIds(dto.Ids); - foreach (var stream in streams) - { - stream.Visible = dto.Visibility; - _unitOfWork.UserRepository.Update(stream); - } - - await _unitOfWork.CommitAsync(); - await _eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId), - userId); - } - - public async Task CreateSideNavStreamFromSmartFilter(int userId, int smartFilterId) - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.SideNavStreams); - if (user == null) throw new KavitaException(await _localizationService.Translate(userId, "no-user")); - - var smartFilter = await _unitOfWork.AppUserSmartFilterRepository.GetById(smartFilterId); - if (smartFilter == null) throw new KavitaException(await _localizationService.Translate(userId, "smart-filter-doesnt-exist")); - - var stream = user.SideNavStreams.FirstOrDefault(d => d.SmartFilter?.Id == smartFilterId); - if (stream != null) throw new KavitaException(await _localizationService.Translate(userId, "smart-filter-already-in-use")); - - var maxOrder = user!.SideNavStreams.Max(d => d.Order); - var createdStream = new AppUserSideNavStream() - { - Name = smartFilter.Name, - IsProvided = false, - StreamType = SideNavStreamType.SmartFilter, - Visible = true, - Order = maxOrder + 1, - SmartFilter = smartFilter - }; - - user.SideNavStreams.Add(createdStream); - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); - - var ret = new SideNavStreamDto() - { - Id = createdStream.Id, - Name = createdStream.Name, - IsProvided = createdStream.IsProvided, - Visible = createdStream.Visible, - Order = createdStream.Order, - SmartFilterEncoded = smartFilter.Filter, - StreamType = createdStream.StreamType - }; - - - await _eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId), - userId); - return ret; - } - - public async Task CreateSideNavStreamFromExternalSource(int userId, int externalSourceId) - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.SideNavStreams); - if (user == null) throw new KavitaException(await _localizationService.Translate(userId, "no-user")); - - var externalSource = await _unitOfWork.AppUserExternalSourceRepository.GetById(externalSourceId); - if (externalSource == null) throw new KavitaException(await _localizationService.Translate(userId, "external-source-doesnt-exist")); - - var stream = user?.SideNavStreams.FirstOrDefault(d => d.ExternalSourceId == externalSourceId); - if (stream != null) throw new KavitaException(await _localizationService.Translate(userId, "external-source-already-in-use")); - - var maxOrder = user!.SideNavStreams.Max(d => d.Order); - var createdStream = new AppUserSideNavStream() - { - Name = externalSource.Name, - IsProvided = false, - StreamType = SideNavStreamType.ExternalSource, - Visible = true, - Order = maxOrder + 1, - ExternalSourceId = externalSource.Id - }; - - user.SideNavStreams.Add(createdStream); - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); - - var ret = new SideNavStreamDto() - { - Name = createdStream.Name, - IsProvided = createdStream.IsProvided, - Visible = createdStream.Visible, - Order = createdStream.Order, - StreamType = createdStream.StreamType, - ExternalSource = new ExternalSourceDto() - { - Host = externalSource.Host, - Id = externalSource.Id, - Name = externalSource.Name, - ApiKey = externalSource.ApiKey - } - }; - - - await _eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId), - userId); - return ret; - } - - public async Task UpdateSideNavStream(int userId, SideNavStreamDto dto) - { - var stream = await _unitOfWork.UserRepository.GetSideNavStream(dto.Id); - if (stream == null) - throw new KavitaException(await _localizationService.Translate(userId, "sidenav-stream-doesnt-exist")); - - stream.Visible = dto.Visible; - - _unitOfWork.UserRepository.Update(stream); - await _unitOfWork.CommitAsync(); - await _eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId), - userId); - } - - public async Task UpdateSideNavStreamPosition(int userId, UpdateStreamPositionDto dto) - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, - AppUserIncludes.SideNavStreams); - var stream = user?.SideNavStreams.FirstOrDefault(d => d.Id == dto.Id); - if (stream == null) throw new KavitaException(await _localizationService.Translate(userId, "sidenav-stream-doesnt-exist")); - - if (stream.Order == dto.ToPosition) return; - - var list = user!.SideNavStreams.OrderBy(s => s.Order).ToList(); - - var wantedPosition = dto.ToPosition; - if (!dto.PositionIncludesInvisible) - { - var visibleItems = list.Where(i => i.Visible).ToList(); - if (dto.ToPosition < 0 || dto.ToPosition >= visibleItems.Count) return; - - var itemAtWantedPosition = visibleItems[dto.ToPosition]; - wantedPosition = list.IndexOf(itemAtWantedPosition); - } - - OrderableHelper.ReorderItems(list, stream.Id, wantedPosition); - user.SideNavStreams = list; - - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); - if (!stream.Visible) return; - await _eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId), - userId); - } - - public async Task CreateExternalSource(int userId, ExternalSourceDto dto) - { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, - AppUserIncludes.ExternalSources); - if (user == null) throw new KavitaException("not-authenticated"); - - if (user.ExternalSources.Any(s => s.Host == dto.Host)) - { - throw new KavitaException("external-source-already-exists"); - } - - if (string.IsNullOrEmpty(dto.ApiKey) || string.IsNullOrEmpty(dto.Name)) throw new KavitaException("external-source-required"); - if (!UrlHelper.StartsWithHttpOrHttps(dto.Host)) throw new KavitaException("external-source-host-format"); - - - var newSource = new AppUserExternalSource() - { - Name = dto.Name, - Host = UrlHelper.EnsureEndsWithSlash( - UrlHelper.EnsureStartsWithHttpOrHttps(dto.Host)), - ApiKey = dto.ApiKey - }; - user.ExternalSources.Add(newSource); - - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); - - dto.Id = newSource.Id; - - return dto; - } - - public async Task UpdateExternalSource(int userId, ExternalSourceDto dto) - { - var source = await _unitOfWork.AppUserExternalSourceRepository.GetById(dto.Id); - if (source == null) throw new KavitaException("external-source-doesnt-exist"); - if (source.AppUserId != userId) throw new KavitaException("external-source-doesnt-exist"); - - if (string.IsNullOrEmpty(dto.ApiKey) || string.IsNullOrEmpty(dto.Host) || string.IsNullOrEmpty(dto.Name)) throw new KavitaException("external-source-required"); - - source.Host = UrlHelper.EnsureEndsWithSlash( - UrlHelper.EnsureStartsWithHttpOrHttps(dto.Host)); - source.ApiKey = dto.ApiKey; - source.Name = dto.Name; - - _unitOfWork.AppUserExternalSourceRepository.Update(source); - await _unitOfWork.CommitAsync(); - - dto.Host = source.Host; - return dto; - } - - public async Task DeleteExternalSource(int userId, int externalSourceId) - { - var source = await _unitOfWork.AppUserExternalSourceRepository.GetById(externalSourceId); - if (source == null) throw new KavitaException("external-source-doesnt-exist"); - if (source.AppUserId != userId) throw new KavitaException("external-source-doesnt-exist"); - - _unitOfWork.AppUserExternalSourceRepository.Delete(source); - - // Find all SideNav's with this source and delete them as well - var streams2 = await _unitOfWork.UserRepository.GetSideNavStreamWithExternalSource(externalSourceId); - _unitOfWork.UserRepository.Delete(streams2); - - await _unitOfWork.CommitAsync(); - } - - public async Task DeleteSideNavSmartFilterStream(int userId, int sideNavStreamId) - { - try - { - var stream = await _unitOfWork.UserRepository.GetSideNavStream(sideNavStreamId); - if (stream == null) throw new KavitaException("sidenav-stream-doesnt-exist"); - - if (stream.AppUserId != userId) throw new KavitaException("sidenav-stream-doesnt-exist"); - - - if (stream.StreamType != SideNavStreamType.SmartFilter) - { - throw new KavitaException("sidenav-stream-only-delete-smart-filter"); - } - - _unitOfWork.UserRepository.Delete(stream); - - await _unitOfWork.CommitAsync(); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an exception deleting SideNav Smart Filter Stream: {FilterId}", sideNavStreamId); - throw; - } - } - - public async Task DeleteDashboardSmartFilterStream(int userId, int dashboardStreamId) - { - try - { - var stream = await _unitOfWork.UserRepository.GetDashboardStream(dashboardStreamId); - if (stream == null) throw new KavitaException("dashboard-stream-doesnt-exist"); - - if (stream.AppUserId != userId) throw new KavitaException("dashboard-stream-doesnt-exist"); - - if (stream.StreamType != DashboardStreamType.SmartFilter) - { - throw new KavitaException("dashboard-stream-only-delete-smart-filter"); - } - - _unitOfWork.UserRepository.Delete(stream); - - await _unitOfWork.CommitAsync(); - } catch (Exception ex) - { - _logger.LogError(ex, "There was an exception deleting Dashboard Smart Filter Stream: {FilterId}", dashboardStreamId); - throw; - } - } - - public async Task RenameSmartFilterStreams(AppUserSmartFilter smartFilter) - { - var sideNavStreams = await _unitOfWork.UserRepository.GetSideNavStreamWithFilter(smartFilter.Id); - var dashboardStreams = await _unitOfWork.UserRepository.GetDashboardStreamWithFilter(smartFilter.Id); - - foreach (var sideNavStream in sideNavStreams) - { - sideNavStream.Name = smartFilter.Name; - } - - foreach (var dashboardStream in dashboardStreams) - { - dashboardStream.Name = smartFilter.Name; - } - - await _unitOfWork.CommitAsync(); - } -} diff --git a/API/Services/Tasks/BackupService.cs b/API/Services/Tasks/BackupService.cs deleted file mode 100644 index f45c1c05e..000000000 --- a/API/Services/Tasks/BackupService.cs +++ /dev/null @@ -1,311 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Entities.Enums; -using API.Logging; -using API.SignalR; -using Hangfire; -using Kavita.Common.EnvironmentInfo; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Services.Tasks; -#nullable enable - -public interface IBackupService -{ - Task BackupDatabase(); - /// - /// Returns a list of all log files for Kavita - /// - /// If file rolling is enabled. Defaults to True. - /// - IEnumerable GetLogFiles(bool rollFiles = LogLevelOptions.LogRollingEnabled); -} -public class BackupService : IBackupService -{ - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - private readonly IDirectoryService _directoryService; - private readonly IEventHub _eventHub; - - private readonly IList _backupFiles; - - public BackupService(ILogger logger, IUnitOfWork unitOfWork, - IDirectoryService directoryService, IEventHub eventHub) - { - _unitOfWork = unitOfWork; - _logger = logger; - _directoryService = directoryService; - _eventHub = eventHub; - - _backupFiles = - [ - "appsettings.json" - ]; - } - - /// - /// Returns a list of all log files for Kavita - /// - /// If file rolling is enabled. Defaults to True. - /// - public IEnumerable GetLogFiles(bool rollFiles = LogLevelOptions.LogRollingEnabled) - { - var multipleFileRegex = rollFiles ? @"\d*" : string.Empty; - var fi = _directoryService.FileSystem.FileInfo.New(LogLevelOptions.LogFile); - - var files = rollFiles - ? _directoryService.GetFiles(_directoryService.LogDirectory, - $@"{_directoryService.FileSystem.Path.GetFileNameWithoutExtension(fi.Name)}{multipleFileRegex}\.log") - : [_directoryService.FileSystem.Path.Join(_directoryService.LogDirectory, "kavita.log")]; - return files; - } - - /// - /// Will back up anything that needs to be backed up. This includes logs, setting files, bare minimum cover images (just locked and first cover). - /// - [AutomaticRetry(Attempts = 3, LogEvents = false, OnAttemptsExceeded = AttemptsExceededAction.Fail)] - public async Task BackupDatabase() - { - _logger.LogInformation("Beginning backup of Database at {BackupTime}", DateTime.Now); - var backupDirectory = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BackupDirectory)).Value; - - _logger.LogDebug("Backing up to {BackupDirectory}", backupDirectory); - if (!_directoryService.ExistOrCreate(backupDirectory)) - { - _logger.LogCritical("Could not write to {BackupDirectory}; aborting backup", backupDirectory); - await _eventHub.SendMessageAsync(MessageFactory.Error, - MessageFactory.ErrorEvent("Backup Service Error",$"Could not write to {backupDirectory}; aborting backup")); - return; - } - - await SendProgress(0F, "Started backup"); - await SendProgress(0.1F, "Copying core files"); - - var dateString = $"{DateTime.UtcNow.ToShortDateString()}_{DateTime.UtcNow:s}Z".Replace("/", "_").Replace(":", "_"); - var zipPath = _directoryService.FileSystem.Path.Join(backupDirectory, $"kavita_backup_v{BuildInfo.Version}_{dateString}.zip"); - - if (File.Exists(zipPath)) - { - _logger.LogCritical("{ZipFile} already exists, aborting", zipPath); - await _eventHub.SendMessageAsync(MessageFactory.Error, - MessageFactory.ErrorEvent("Backup Service Error",$"{zipPath} already exists, aborting")); - return; - } - - var tempDirectory = Path.Join(_directoryService.TempDirectory, dateString); - _directoryService.ExistOrCreate(tempDirectory); - _directoryService.ClearDirectory(tempDirectory); - - await SendProgress(0.1F, "Backing up database"); - await BackupDatabaseFile(tempDirectory); - - await SendProgress(0.15F, "Copying config files"); - _directoryService.CopyFilesToDirectory( - _backupFiles.Select(file => _directoryService.FileSystem.Path.Join(_directoryService.ConfigDirectory, file)), tempDirectory); - - // Copy any csv's as those are used for manual migrations - _directoryService.CopyFilesToDirectory( - _directoryService.GetFilesWithCertainExtensions(_directoryService.ConfigDirectory, @"\.csv"), tempDirectory); - - await SendProgress(0.2F, "Copying logs"); - CopyLogsToBackupDirectory(tempDirectory); - - await SendProgress(0.25F, "Copying cover images"); - await CopyCoverImagesToBackupDirectory(tempDirectory); - - await SendProgress(0.35F, "Copying templates images"); - CopyTemplatesToBackupDirectory(tempDirectory); - - await SendProgress(0.5F, "Copying bookmarks"); - await CopyBookmarksToBackupDirectory(tempDirectory); - - await SendProgress(0.6F, "Copying Fonts"); - CopyFontsToBackupDirectory(tempDirectory); - - await SendProgress(0.75F, "Copying themes"); - CopyThemesToBackupDirectory(tempDirectory); - - await SendProgress(0.85F, "Copying favicons"); - CopyFaviconsToBackupDirectory(tempDirectory); - - try - { - await ZipFile.CreateFromDirectoryAsync(tempDirectory, zipPath); - } - catch (AggregateException ex) - { - _logger.LogError(ex, "There was an issue when archiving library backup"); - } - - _directoryService.ClearAndDeleteDirectory(tempDirectory); - _logger.LogInformation("Database backup completed"); - await SendProgress(1F, "Completed backup"); - } - - private void CopyLogsToBackupDirectory(string tempDirectory) - { - var files = GetLogFiles(); - _directoryService.CopyFilesToDirectory(files, _directoryService.FileSystem.Path.Join(tempDirectory, "logs")); - } - - /// - /// Creates a backup of the SQLite database using VACUUM INTO command. - /// This method safely backs up the database while it's in use, without locking issues. - /// - /// The directory where the backup file will be created - private async Task BackupDatabaseFile(string tempDirectory) - { - var backupPath = _directoryService.FileSystem.Path.Join(tempDirectory, "kavita.db"); - - // Validate the backup path to prevent SQL injection - // The path must not contain single quotes which could break the SQL command - if (backupPath.Contains('\'')) - { - throw new ArgumentException("Backup path contains invalid characters", nameof(tempDirectory)); - } - - try - { - // Use VACUUM INTO to create a safe backup of the database while it's running - // This creates a consistent snapshot without locking the main database - // Note: VACUUM INTO requires a literal path and cannot use SQL parameters - #pragma warning disable EF1002 // The backup path is validated above to not contain SQL injection characters - await _unitOfWork.DataContext.Database.ExecuteSqlRawAsync($"VACUUM INTO '{backupPath}'"); - #pragma warning restore EF1002 - _logger.LogDebug("Database backup created successfully at {BackupPath}", backupPath); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to create database backup using VACUUM INTO at {BackupPath}", backupPath); - throw new InvalidOperationException($"Failed to create database backup at {backupPath}", ex); - } - } - - private void CopyFaviconsToBackupDirectory(string tempDirectory) - { - _directoryService.CopyDirectoryToDirectory(_directoryService.FaviconDirectory, _directoryService.FileSystem.Path.Join(tempDirectory, "favicons")); - } - - private void CopyTemplatesToBackupDirectory(string tempDirectory) - { - _directoryService.CopyDirectoryToDirectory(_directoryService.TemplateDirectory, _directoryService.FileSystem.Path.Join(tempDirectory, "templates")); - } - - private async Task CopyCoverImagesToBackupDirectory(string tempDirectory) - { - var outputTempDir = Path.Join(tempDirectory, "covers"); - _directoryService.ExistOrCreate(outputTempDir); - - try - { - var seriesImages = await _unitOfWork.SeriesRepository.GetLockedCoverImagesAsync(); - _directoryService.CopyFilesToDirectory( - seriesImages.Select(s => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, s)), outputTempDir); - - var collectionTags = await _unitOfWork.CollectionTagRepository.GetAllCoverImagesAsync(); - _directoryService.CopyFilesToDirectory( - collectionTags.Select(s => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, s)), outputTempDir); - - var chapterImages = await _unitOfWork.ChapterRepository.GetCoverImagesForLockedChaptersAsync(); - _directoryService.CopyFilesToDirectory( - chapterImages.Select(s => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, s)), outputTempDir); - - var volumeImages = await _unitOfWork.VolumeRepository.GetCoverImagesForLockedVolumesAsync(); - _directoryService.CopyFilesToDirectory( - volumeImages.Select(s => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, s)), outputTempDir); - - var libraryImages = await _unitOfWork.LibraryRepository.GetAllCoverImagesAsync(); - _directoryService.CopyFilesToDirectory( - libraryImages.Select(s => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, s)), outputTempDir); - - var readingListImages = await _unitOfWork.ReadingListRepository.GetAllCoverImagesAsync(); - _directoryService.CopyFilesToDirectory( - readingListImages.Select(s => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, s)), outputTempDir); - } - catch (IOException) - { - // Swallow exception. This can be a duplicate cover being copied as chapter and volumes can share same file. - } - - if (!_directoryService.GetFiles(outputTempDir, searchOption: SearchOption.AllDirectories).Any()) - { - _directoryService.ClearAndDeleteDirectory(outputTempDir); - } - } - - private async Task CopyBookmarksToBackupDirectory(string tempDirectory) - { - var bookmarkDirectory = - (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; - - var outputTempDir = Path.Join(tempDirectory, "bookmarks"); - _directoryService.ExistOrCreate(outputTempDir); - - try - { - _directoryService.CopyDirectoryToDirectory(bookmarkDirectory, outputTempDir); - } - catch (IOException) - { - // Swallow exception. - } - - if (!_directoryService.GetFiles(outputTempDir, searchOption: SearchOption.AllDirectories).Any()) - { - _directoryService.ClearAndDeleteDirectory(outputTempDir); - } - } - - private void CopyFontsToBackupDirectory(string tempDirectory) - { - var outputTempDir = Path.Join(tempDirectory, "fonts"); - _directoryService.ExistOrCreate(outputTempDir); - - try - { - _directoryService.CopyDirectoryToDirectory(_directoryService.EpubFontDirectory, outputTempDir); - } - catch (IOException ex) - { - _logger.LogWarning(ex, "Failed to copy fonts to backup directory '{OutputTempDir}'. Fonts will not be included in the backup.", outputTempDir); - } - - if (!_directoryService.GetFiles(outputTempDir, searchOption: SearchOption.AllDirectories).Any()) - { - _directoryService.ClearAndDeleteDirectory(outputTempDir); - } - } - - private void CopyThemesToBackupDirectory(string tempDirectory) - { - var outputTempDir = Path.Join(tempDirectory, "themes"); - _directoryService.ExistOrCreate(outputTempDir); - - try - { - _directoryService.CopyDirectoryToDirectory(_directoryService.SiteThemeDirectory, outputTempDir); - } - catch (IOException) - { - // Swallow exception. - } - - if (!_directoryService.GetFiles(outputTempDir, searchOption: SearchOption.AllDirectories).Any()) - { - _directoryService.ClearAndDeleteDirectory(outputTempDir); - } - } - - private async Task SendProgress(float progress, string subtitle) - { - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.BackupDatabaseProgressEvent(progress, subtitle)); - } - -} diff --git a/API/Services/Tasks/CleanupService.cs b/API/Services/Tasks/CleanupService.cs deleted file mode 100644 index 1884805bc..000000000 --- a/API/Services/Tasks/CleanupService.cs +++ /dev/null @@ -1,449 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Filtering; -using API.Entities; -using API.Entities.Enums; -using API.Helpers; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; -using Hangfire; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Services.Tasks; -#nullable enable - -public interface ICleanupService -{ - Task Cleanup(); - Task CleanupDbEntries(); - Task CleanupCacheAndTempDirectories(); - void CleanupCacheDirectory(); - Task DeleteSeriesCoverImages(); - Task DeleteChapterCoverImages(); - Task DeleteTagCoverImages(); - Task CleanupBackups(); - Task CleanupLogs(); - void CleanupTemp(); - Task EnsureChapterProgressIsCapped(); - /// - /// Responsible to remove Series from Want To Read when user's have fully read the series and the series has Publication Status of Completed or Cancelled. - /// - /// - Task CleanupWantToRead(); - - Task ConsolidateProgress(); - - Task CleanupMediaErrors(); - -} -/// -/// Cleans up after operations on reoccurring basis -/// -public class CleanupService : ICleanupService -{ - private readonly ILogger _logger; - private readonly IUnitOfWork _unitOfWork; - private readonly IEventHub _eventHub; - private readonly IDirectoryService _directoryService; - - public CleanupService(ILogger logger, - IUnitOfWork unitOfWork, IEventHub eventHub, - IDirectoryService directoryService) - { - _logger = logger; - _unitOfWork = unitOfWork; - _eventHub = eventHub; - _directoryService = directoryService; - } - - - /// - /// Cleans up Temp, cache, deleted cover images, and old database backups - /// - [AutomaticRetry(Attempts = 3, LogEvents = false, OnAttemptsExceeded = AttemptsExceededAction.Fail, DelaysInSeconds = [120, 300, 300])] - public async Task Cleanup() - { - if (TaskScheduler.HasAlreadyEnqueuedTask(BookmarkService.Name, "ConvertAllCoverToEncoding", [], - TaskScheduler.DefaultQueue, true) || - TaskScheduler.HasAlreadyEnqueuedTask(BookmarkService.Name, "ConvertAllBookmarkToEncoding", [], - TaskScheduler.DefaultQueue, true)) - { - _logger.LogInformation("Cleanup put on hold as a media conversion in progress"); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.ErrorEvent("Cleanup", "Cleanup put on hold as a media conversion in progress")); - return; - } - - _logger.LogInformation("Starting Cleanup"); - - // TODO: Why do I have clear temp directory then immediately do it again? - var cleanupSteps = new List<(Func, string)> - { - (() => Task.Run(() => _directoryService.ClearDirectory(_directoryService.TempDirectory)), "Cleaning temp directory"), - (CleanupCacheAndTempDirectories, "Cleaning cache and temp directories"), - (CleanupBackups, "Cleaning old database backups"), - (ConsolidateProgress, "Consolidating Progress Events"), - (CleanupMediaErrors, "Consolidating Media Errors"), - (CleanupDbEntries, "Cleaning abandoned database rows"), // Cleanup DB before removing files linked to DB entries - (DeleteSeriesCoverImages, "Cleaning deleted series cover images"), - (DeleteChapterCoverImages, "Cleaning deleted chapter cover images"), - (() => Task.WhenAll(DeleteTagCoverImages(), DeleteReadingListCoverImages(), DeletePersonCoverImages()), "Cleaning deleted cover images"), - (CleanupLogs, "Cleaning old logs"), - (EnsureChapterProgressIsCapped, "Cleaning progress events that exceed 100%") - }; - - await SendProgress(0F, "Starting cleanup"); - - for (var i = 0; i < cleanupSteps.Count; i++) - { - var (method, subtitle) = cleanupSteps[i]; - var progress = (float)(i + 1) / (cleanupSteps.Count + 1); - - _logger.LogInformation("{Message}", subtitle); - await method(); - await SendProgress(progress, subtitle); - } - - await SendProgress(1F, "Cleanup finished"); - _logger.LogInformation("Cleanup finished"); - } - - /// - /// Cleans up abandon rows in the DB - /// - public async Task CleanupDbEntries() - { - await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters(); - await _unitOfWork.PersonRepository.RemoveAllPeopleNoLongerAssociated(); - await _unitOfWork.GenreRepository.RemoveAllGenreNoLongerAssociated(); - await _unitOfWork.CollectionTagRepository.RemoveCollectionsWithoutSeries(); - await _unitOfWork.ReadingListRepository.RemoveReadingListsWithoutSeries(); - } - - private async Task SendProgress(float progress, string subtitle) - { - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.CleanupProgressEvent(progress, subtitle)); - } - - /// - /// Removes all series images that are not in the database. They must follow filename pattern. - /// - public async Task DeleteSeriesCoverImages() - { - var images = await _unitOfWork.SeriesRepository.GetAllCoverImagesAsync(); - var files = _directoryService.GetFiles(_directoryService.CoverImageDirectory, ImageService.SeriesCoverImageRegex); - _directoryService.DeleteFiles(files.Where(file => !images.Contains(_directoryService.FileSystem.Path.GetFileName(file)))); - } - - /// - /// Removes all chapter/volume images that are not in the database. They must follow filename pattern. - /// - public async Task DeleteChapterCoverImages() - { - var images = await _unitOfWork.ChapterRepository.GetAllCoverImagesAsync(); - var files = _directoryService.GetFiles(_directoryService.CoverImageDirectory, ImageService.ChapterCoverImageRegex); - _directoryService.DeleteFiles(files.Where(file => !images.Contains(_directoryService.FileSystem.Path.GetFileName(file)))); - } - - /// - /// Removes all collection tag images that are not in the database. They must follow filename pattern. - /// - public async Task DeleteTagCoverImages() - { - var images = await _unitOfWork.CollectionTagRepository.GetAllCoverImagesAsync(); - var files = _directoryService.GetFiles(_directoryService.CoverImageDirectory, ImageService.CollectionTagCoverImageRegex); - _directoryService.DeleteFiles(files.Where(file => !images.Contains(_directoryService.FileSystem.Path.GetFileName(file)))); - } - - /// - /// Removes all reading list images that are not in the database. They must follow filename pattern. - /// - public async Task DeleteReadingListCoverImages() - { - var images = await _unitOfWork.ReadingListRepository.GetAllCoverImagesAsync(); - var files = _directoryService.GetFiles(_directoryService.CoverImageDirectory, ImageService.ReadingListCoverImageRegex); - _directoryService.DeleteFiles(files.Where(file => !images.Contains(_directoryService.FileSystem.Path.GetFileName(file)))); - } - - /// - /// Remove all person cover images no longer associated with a person in the database - /// - public async Task DeletePersonCoverImages() - { - var images = await _unitOfWork.PersonRepository.GetAllCoverImagesAsync(); - var files = _directoryService.GetFiles(_directoryService.CoverImageDirectory, ImageService.PersonCoverImageRegex); - _directoryService.DeleteFiles(files.Where(file => !images.Contains(_directoryService.FileSystem.Path.GetFileName(file)))); - } - - /// - /// Removes all files and directories in the cache and temp directory - /// - public Task CleanupCacheAndTempDirectories() - { - _logger.LogInformation("Performing cleanup of Cache & Temp directories"); - _directoryService.ExistOrCreate(_directoryService.CacheDirectory); - _directoryService.ExistOrCreate(_directoryService.TempDirectory); - - try - { - _directoryService.ClearDirectory(_directoryService.CacheDirectory); - _directoryService.ClearDirectory(_directoryService.TempDirectory); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an issue deleting one or more folders/files during cleanup"); - } - - _logger.LogInformation("Cache and temp directory purged"); - - return Task.CompletedTask; - } - - public void CleanupCacheDirectory() - { - _logger.LogInformation("Performing cleanup of Cache directories"); - _directoryService.ExistOrCreate(_directoryService.CacheDirectory); - - try - { - _directoryService.ClearDirectory(_directoryService.CacheDirectory); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an issue deleting one or more folders/files during cleanup"); - } - - _logger.LogInformation("Cache directory purged"); - } - - /// - /// Removes Database backups older than configured total backups. If all backups are older than total backups days, only the latest is kept. - /// - public async Task CleanupBackups() - { - var dayThreshold = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).TotalBackups; - _logger.LogInformation("Beginning cleanup of Database backups at {Time}", DateTime.Now); - var backupDirectory = - (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BackupDirectory)).Value; - if (!_directoryService.Exists(backupDirectory)) return; - - var deltaTime = DateTime.Today.Subtract(TimeSpan.FromDays(dayThreshold)); - var allBackups = _directoryService.GetFiles(backupDirectory).ToList(); - var expiredBackups = allBackups.Select(filename => _directoryService.FileSystem.FileInfo.New(filename)) - .Where(f => f.CreationTime < deltaTime) - .ToList(); - - if (expiredBackups.Count == allBackups.Count) - { - _logger.LogInformation("All expired backups are older than {Threshold} days. Removing all but last backup", dayThreshold); - var toDelete = expiredBackups.OrderByDescending(f => f.CreationTime).ToList(); - _directoryService.DeleteFiles(toDelete.Take(toDelete.Count - 1).Select(f => f.FullName)); - } - else - { - _directoryService.DeleteFiles(expiredBackups.Select(f => f.FullName)); - } - _logger.LogInformation("Finished cleanup of Database backups at {Time}", DateTime.Now); - } - - /// - /// Find any progress events that have duplicate, find the highest page read event, then copy over information from that and delete others, to leave one. - /// - public async Task ConsolidateProgress() - { - _logger.LogInformation("Consolidating Progress Events"); - // AppUserProgress - var allProgress = await _unitOfWork.AppUserProgressRepository.GetAllProgress(); - - // Group by the unique identifiers that would make a progress entry unique - var duplicateGroups = allProgress - .GroupBy(p => new - { - p.AppUserId, - p.ChapterId, - }) - .Where(g => g.Count() > 1); - - foreach (var group in duplicateGroups) - { - // Find the entry with the highest pages read - var highestProgress = group - .OrderByDescending(p => p.PagesRead) - .ThenByDescending(p => p.LastModifiedUtc) - .First(); - - // Get the duplicate entries to remove (all except the highest progress) - var duplicatesToRemove = group - .Where(p => p.Id != highestProgress.Id) - .ToList(); - - // Copy over any non-null BookScrollId if the highest progress entry doesn't have one - if (string.IsNullOrEmpty(highestProgress.BookScrollId)) - { - var firstValidScrollId = duplicatesToRemove - .FirstOrDefault(p => !string.IsNullOrEmpty(p.BookScrollId)) - ?.BookScrollId; - - if (firstValidScrollId != null) - { - highestProgress.BookScrollId = firstValidScrollId; - highestProgress.MarkModified(); - } - } - - // Remove the duplicates - foreach (var duplicate in duplicatesToRemove) - { - _unitOfWork.AppUserProgressRepository.Remove(duplicate); - } - } - - // Save changes - await _unitOfWork.CommitAsync(); - } - - /// - /// Scans through Media Error and removes any entries that have been fixed and are within the DB (proper files where wordcount/pagecount > 0) - /// - public async Task CleanupMediaErrors() - { - try - { - List errorStrings = ["This archive cannot be read or not supported", "File format not supported"]; - var mediaErrors = await _unitOfWork.MediaErrorRepository.GetAllErrorsAsync(errorStrings); - _logger.LogInformation("Beginning consolidation of {Count} Media Errors", mediaErrors.Count); - - var pathToErrorMap = mediaErrors - .GroupBy(me => Parser.NormalizePath(me.FilePath)) - .ToDictionary( - group => group.Key, - group => group.ToList() // The same file can be duplicated (rare issue when network drives die out midscan) - ); - - var normalizedPaths = pathToErrorMap.Keys.ToList(); - - // Find all files that are valid - var validFiles = await _unitOfWork.DataContext.MangaFile - .Where(f => normalizedPaths.Contains(f.FilePath) && f.Pages > 0) - .Select(f => f.FilePath) - .ToListAsync(); - - var removalCount = 0; - foreach (var validFilePath in validFiles) - { - if (!pathToErrorMap.TryGetValue(validFilePath, out var mediaError)) continue; - - _unitOfWork.MediaErrorRepository.Remove(mediaError); - removalCount++; - } - - await _unitOfWork.CommitAsync(); - - _logger.LogInformation("Finished consolidation of {Count} Media Errors, Removed: {RemovalCount}", - mediaErrors.Count, removalCount); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an exception consolidating media errors"); - } - } - - public async Task CleanupLogs() - { - _logger.LogInformation("Performing cleanup of logs directory"); - var dayThreshold = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).TotalLogs; - var deltaTime = DateTime.Today.Subtract(TimeSpan.FromDays(dayThreshold)); - var allLogs = _directoryService.GetFiles(_directoryService.LogDirectory).ToList(); - var expiredLogs = allLogs.Select(filename => _directoryService.FileSystem.FileInfo.New(filename)) - .Where(f => f.CreationTime < deltaTime) - .ToList(); - - if (expiredLogs.Count == allLogs.Count) - { - _logger.LogInformation("All expired backups are older than {Threshold} days. Removing all but last backup", dayThreshold); - var toDelete = expiredLogs.OrderBy(f => f.CreationTime).ToList(); - _directoryService.DeleteFiles(toDelete.Take(toDelete.Count - 1).Select(f => f.FullName)); - } - else - { - _directoryService.DeleteFiles(expiredLogs.Select(f => f.FullName)); - } - _logger.LogInformation("Finished cleanup of logs at {Time}", DateTime.Now); - } - - public void CleanupTemp() - { - _logger.LogInformation("Performing cleanup of Temp directory"); - _directoryService.ExistOrCreate(_directoryService.TempDirectory); - - try - { - _directoryService.ClearDirectory(_directoryService.TempDirectory); - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an issue deleting one or more folders/files during cleanup"); - } - - _logger.LogInformation("Temp directory purged"); - } - - /// - /// Ensures that each chapter's progress (pages read) is capped at the total pages. This can get out of sync when a chapter is replaced after being read with one with lower page count. - /// - /// - public async Task EnsureChapterProgressIsCapped() - { - _logger.LogInformation("Cleaning up any progress rows that exceed chapter page count"); - await _unitOfWork.AppUserProgressRepository.UpdateAllProgressThatAreMoreThanChapterPages(); - _logger.LogInformation("Cleaning up any progress rows that exceed chapter page count - complete"); - } - - /// - /// This does not cleanup any Series that are not Completed or Cancelled - /// - public async Task CleanupWantToRead() - { - _logger.LogInformation("Performing cleanup of Series that are Completed and have been fully read that are in Want To Read list"); - - var libraryIds = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).Select(l => l.Id).ToList(); - var filter = new FilterDto() - { - PublicationStatus = new List() - { - PublicationStatus.Completed, - PublicationStatus.Cancelled - }, - Libraries = libraryIds, - ReadStatus = new ReadStatus() - { - Read = true, - InProgress = false, - NotRead = false - } - }; - foreach (var user in await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.WantToRead)) - { - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(0, user.Id, new UserParams(), filter); - 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.SeriesId)).ToList(); - _unitOfWork.UserRepository.Update(user); - } - - if (_unitOfWork.HasChanges()) - { - await _unitOfWork.CommitAsync(); - } - - _logger.LogInformation("Performing cleanup of Series that are Completed and have been fully read that are in Want To Read list, completed"); - } -} diff --git a/API/SignalR/EventHub.cs b/API/SignalR/EventHub.cs deleted file mode 100644 index 2df394fe9..000000000 --- a/API/SignalR/EventHub.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Linq; -using System.Collections.Generic; -using System.Threading.Tasks; -using API.SignalR.Presence; -using Microsoft.AspNetCore.SignalR; - -namespace API.SignalR; - -/// -/// Responsible for ushering events to the UI and allowing simple DI hook to send data -/// -public interface IEventHub -{ - Task SendMessageAsync(string method, SignalRMessage message, bool onlyAdmins = true); - Task SendMessageToAsync(string method, SignalRMessage message, int userId); -} - -public class EventHub : IEventHub -{ - private readonly IHubContext _messageHub; - private readonly IPresenceTracker _presenceTracker; - - public EventHub(IHubContext messageHub, IPresenceTracker presenceTracker) - { - _messageHub = messageHub; - _presenceTracker = presenceTracker; - - // TODO: When sending a message, queue the message up and on re-connect, reply the queued messages. Queue messages expire on a rolling basis (rolling array) - } - - public async Task SendMessageAsync(string method, SignalRMessage message, bool onlyAdmins = true) - { - // TODO: If libraryId and NOT onlyAdmins, then perform RBS check before sending the event - - var users = _messageHub.Clients.All; - if (onlyAdmins) - { - var admins = await _presenceTracker.GetOnlineAdminIds(); - users = _messageHub.Clients.Users(admins.Select(i => i.ToString()).ToArray()); - } - - - await users.SendAsync(method, message); - } - - /// - /// Sends a message directly to a user if they are connected - /// - /// - /// - /// - /// - public async Task SendMessageToAsync(string method, SignalRMessage message, int userId) - { - await _messageHub.Clients.Users(new List() {userId + string.Empty}).SendAsync(method, message); - } - -} diff --git a/API/redo-migration.sh b/API/redo-migration.sh deleted file mode 100755 index a1ef82bc0..000000000 --- a/API/redo-migration.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - -migrations=($(ls -1 Data/Migrations/*.cs 2>/dev/null | grep -v "Designer.cs" | grep -v "Snapshot.cs" | sort -r)) - -if [ ${#migrations[@]} -lt 2 ]; then - echo "Error: Need at least 2 migrations to redo" - exit 1 -fi - -second_last=$(basename "${migrations[1]}" .cs) -last=$(basename "${migrations[0]}" .cs) -last_name=$(echo "$last" | sed 's/^[0-9]*_//') - -new_name=${1:-$last_name} - -echo "Rolling back to: $second_last" -echo "Removing $last_name and re-adding as $new_name" -read -p "Continue? (y/N) " -n 1 -r -echo "" - -if [[ $REPLY =~ ^[Yy]$ ]]; then - dotnet ef database update "$second_last" && \ - dotnet ef migrations remove && \ - dotnet ef migrations add "$new_name" -else - echo "Cancelled" - exit 0 -fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7bb7bd79d..ce51ce4ef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,14 +26,17 @@ Setup guides, FAQ, the more information we have on the [wiki](https://wiki.kavit - `npm install -g @angular/cli` 5. Start the frontend - `npm run start` -6. Build the project in Visual Studio/Rider, Setting startup project to `API` +6. Build the project in Visual Studio/Rider, Setting startup project to `Kavita.Server (Server)` 7. Debug the project in Visual Studio/Rider 8. Open http://localhost:4200 9. (Deployment only) Run build.sh and pass the Runtime Identifier for your OS or just build.sh for all supported RIDs. ### Debugging on Device ### -- Update `IP` constant in `Web/UI/src/environments/environment.ts` to your dev machine's ip instead of `localhost`. +- Run `npm run start-proxy` instead to have the Angular application proxy the requests to the backend. +### Apple users + +The backend may fail to start due to port 5000 already being in use. To fix this, temporally turn off AirPlay Receiver in System Settings. You can re-enable it later, it will bind to a different port. You may need to do this again after an update or reboot. ### Contributing Code ### - If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Kareadita/Kavita/issues "Github Issues") so work is not duplicated (If you want to add something not already on there, please talk to us first) @@ -61,7 +64,7 @@ Setup guides, FAQ, the more information we have on the [wiki](https://wiki.kavit ### Swagger API ### If you just want to play with Swagger, you can just -- cd Kavita/API +- cd Kavita/Kavita.Server - dotnet run -c Debug - Go to http://localhost:5000/swagger/index.html diff --git a/Dockerfile b/Dockerfile index 04e13304f..057137da6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ FROM ubuntu:noble COPY --from=copytask /Kavita /kavita COPY --from=copytask /files/wwwroot /kavita/wwwroot -COPY API/config/appsettings.json /tmp/config/appsettings.json +COPY Kavita.Server/config/appsettings.json /tmp/config/appsettings.json #Installs program dependencies ENV DEBIAN_FRONTEND=noninteractive diff --git a/Kavita.API/Attributes/SkipDeviceTrackingAttribute.cs b/Kavita.API/Attributes/SkipDeviceTrackingAttribute.cs new file mode 100644 index 000000000..7263ef8a0 --- /dev/null +++ b/Kavita.API/Attributes/SkipDeviceTrackingAttribute.cs @@ -0,0 +1,10 @@ +using System; + +namespace Kavita.API.Attributes; + +/// +/// Attribute to skip device tracking on specific endpoints. +/// Use for high-frequency endpoints where device tracking adds unnecessary overhead. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public class SkipDeviceTrackingAttribute : Attribute; diff --git a/Kavita.API/Database/IDataContext.cs b/Kavita.API/Database/IDataContext.cs new file mode 100644 index 000000000..455e3211e --- /dev/null +++ b/Kavita.API/Database/IDataContext.cs @@ -0,0 +1,98 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.Entities; +using Kavita.Models.Entities.History; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.MetadataMatching; +using Kavita.Models.Entities.Person; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.Scrobble; +using Kavita.Models.Entities.User; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Kavita.API.Database; + + +public interface IDataContext : IDisposable +{ + + DatabaseFacade Database { get; } + + DbSet Users { get; } + DbSet Library { get; } + DbSet Series { get; } + DbSet Chapter { get; } + DbSet Volume { get; } + DbSet AppUser { get; } + DbSet MangaFile { get; } + DbSet AppUserProgresses { get; } + DbSet AppUserRating { get; } + DbSet ServerSetting { get; } + DbSet AppUserPreferences { get; } + DbSet SeriesMetadata { get; } + DbSet SeriesMetadataTag { get; } + DbSet GenreSeriesMetadata { get; } + + [Obsolete("Use AppUserCollection")] + DbSet CollectionTag { get; } + + DbSet AppUserBookmark { get; } + DbSet ReadingList { get; } + DbSet ReadingListItem { get; } + DbSet Person { get; } + DbSet PersonAlias { get; } + DbSet Genre { get; } + DbSet Tag { get; } + DbSet SiteTheme { get; } + DbSet SeriesRelation { get; } + DbSet FolderPath { get; } + DbSet Device { get; } + DbSet ServerStatistics { get; } + DbSet MediaError { get; } + DbSet ScrobbleEvent { get; } + DbSet ScrobbleError { get; } + DbSet ScrobbleHold { get; } + DbSet AppUserOnDeckRemoval { get; } + DbSet AppUserTableOfContent { get; } + DbSet AppUserSmartFilter { get; } + DbSet AppUserDashboardStream { get; } + DbSet AppUserSideNavStream { get; } + DbSet AppUserExternalSource { get; } + DbSet ExternalReview { get; } + DbSet ExternalRating { get; } + DbSet ExternalSeriesMetadata { get; } + DbSet ExternalRecommendation { get; } + DbSet ManualMigrationHistory { get; } + + [Obsolete("Use IsBlacklisted field on Series")] + DbSet SeriesBlacklist { get; } + + DbSet AppUserCollection { get; } + DbSet ChapterPeople { get; } + DbSet SeriesMetadataPeople { get; } + DbSet EmailHistory { get; } + DbSet MetadataSettings { get; } + DbSet MetadataFieldMapping { get; } + DbSet AppUserChapterRating { get; } + DbSet AppUserReadingProfiles { get; } + DbSet AppUserAnnotation { get; } + DbSet EpubFont { get; } + DbSet AppUserReadingSession { get; } + DbSet AppUserReadingSessionActivityData { get; } + DbSet AppUserReadingHistory { get; } + DbSet ClientDevice { get; } + DbSet ClientDeviceHistory { get; } + DbSet AppUserAuthKey { get; } + + // Change Tracking and Saving + ChangeTracker ChangeTracker { get; } + int SaveChanges(); + int SaveChanges(bool acceptAllChangesOnSuccess); + Task SaveChangesAsync(CancellationToken cancellationToken = default); + Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default); + + EntityEntry Entry(TEntity entity) where TEntity : class; +} diff --git a/Kavita.API/Database/IUnitOfWork.cs b/Kavita.API/Database/IUnitOfWork.cs new file mode 100644 index 000000000..6c352282d --- /dev/null +++ b/Kavita.API/Database/IUnitOfWork.cs @@ -0,0 +1,42 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.API.Repositories; + +namespace Kavita.API.Database; + +public interface IUnitOfWork +{ + IDataContext DataContext { get; } + ISeriesRepository SeriesRepository { get; } + IUserRepository UserRepository { get; } + ILibraryRepository LibraryRepository { get; } + IVolumeRepository VolumeRepository { get; } + ISettingsRepository SettingsRepository { get; } + IAppUserProgressRepository AppUserProgressRepository { get; } + ICollectionTagRepository CollectionTagRepository { get; } + IChapterRepository ChapterRepository { get; } + IReadingListRepository ReadingListRepository { get; } + ISeriesMetadataRepository SeriesMetadataRepository { get; } + IPersonRepository PersonRepository { get; } + IGenreRepository GenreRepository { get; } + ITagRepository TagRepository { get; } + ISiteThemeRepository SiteThemeRepository { get; } + IMangaFileRepository MangaFileRepository { get; } + IDeviceRepository DeviceRepository { get; } + IMediaErrorRepository MediaErrorRepository { get; } + IScrobbleRepository ScrobbleRepository { get; } + IUserTableOfContentRepository UserTableOfContentRepository { get; } + IAppUserSmartFilterRepository AppUserSmartFilterRepository { get; } + IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; } + IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; } + IEmailHistoryRepository EmailHistoryRepository { get; } + IAppUserReadingProfileRepository AppUserReadingProfileRepository { get; } + IAnnotationRepository AnnotationRepository { get; } + IEpubFontRepository EpubFontRepository { get; } + IReadingSessionRepository ReadingSessionRepository { get; } + IClientDeviceRepository ClientDeviceRepository { get; } + bool Commit(); + Task CommitAsync(CancellationToken ct = default); + bool HasChanges(); + Task RollbackAsync(CancellationToken ct = default); +} diff --git a/API/Errors/ApiException.cs b/Kavita.API/Errors/ApiException.cs similarity index 67% rename from API/Errors/ApiException.cs rename to Kavita.API/Errors/ApiException.cs index 60d93729c..85147985d 100644 --- a/API/Errors/ApiException.cs +++ b/Kavita.API/Errors/ApiException.cs @@ -1,4 +1,3 @@ -namespace API.Errors; +namespace Kavita.API.Errors; -#nullable enable public record ApiException(int Status, string? Message = null, string? Details = null); diff --git a/API/Exceptions/OpdsException.cs b/Kavita.API/Errors/OpdsException.cs similarity index 80% rename from API/Exceptions/OpdsException.cs rename to Kavita.API/Errors/OpdsException.cs index 0267628a2..3e8187105 100644 --- a/API/Exceptions/OpdsException.cs +++ b/Kavita.API/Errors/OpdsException.cs @@ -1,8 +1,6 @@ -using System; -using API.Controllers; -using API.Services; +using System; -namespace API.Exceptions; +namespace Kavita.API.Errors; /// /// Should be caught in and ONLY used in diff --git a/Kavita.API/Kavita.API.csproj b/Kavita.API/Kavita.API.csproj new file mode 100644 index 000000000..63fa90831 --- /dev/null +++ b/Kavita.API/Kavita.API.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + disable + enable + + + + + + + + + + + + + + + + diff --git a/Kavita.API/Repositories/IAnnotationRepository.cs b/Kavita.API/Repositories/IAnnotationRepository.cs new file mode 100644 index 000000000..47474d572 --- /dev/null +++ b/Kavita.API/Repositories/IAnnotationRepository.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Annotations; +using Kavita.Models.DTOs.Metadata.Browse.Requests; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Repositories; + +public interface IAnnotationRepository +{ + void Attach(AppUserAnnotation annotation); + void Update(AppUserAnnotation annotation); + void Remove(AppUserAnnotation annotation); + void Remove(IEnumerable annotations); + Task GetAnnotationDto(int id, CancellationToken ct = default); + Task GetAnnotation(int id, CancellationToken ct = default); + Task> GetAllAnnotations(CancellationToken ct = default); + Task> GetAnnotations(int userId, IList ids, CancellationToken ct = default); + Task> GetFullAnnotationsByUserIdAsync(int userId, CancellationToken ct = default); + Task> GetFullAnnotations(int userId, IList annotationIds, CancellationToken ct = default); + Task> GetAnnotationDtos(int userId, BrowseAnnotationFilterDto filter, UserParams userParams, CancellationToken ct = default); + Task> GetSeriesWithAnnotations(int userId, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IAppUserExternalSourceRepository.cs b/Kavita.API/Repositories/IAppUserExternalSourceRepository.cs new file mode 100644 index 000000000..b6c59f943 --- /dev/null +++ b/Kavita.API/Repositories/IAppUserExternalSourceRepository.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.SideNav; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Repositories; + +public interface IAppUserExternalSourceRepository +{ + void Update(AppUserExternalSource source); + void Delete(AppUserExternalSource source); + Task GetById(int externalSourceId, CancellationToken ct = default); + Task> GetExternalSources(int userId, CancellationToken ct = default); + Task ExternalSourceExists(int userId, string name, string host, string apiKey, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IAppUserProgressRepository.cs b/Kavita.API/Repositories/IAppUserProgressRepository.cs new file mode 100644 index 000000000..6032fb6d2 --- /dev/null +++ b/Kavita.API/Repositories/IAppUserProgressRepository.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; + +namespace Kavita.API.Repositories; + +public interface IAppUserProgressRepository +{ + void Update(AppUserProgress userProgress); + void Remove(AppUserProgress userProgress); + Task CleanupAbandonedChapters(CancellationToken ct = default); + Task UserHasProgress(LibraryType libraryType, int userId, CancellationToken ct = default); + Task GetUserProgressAsync(int chapterId, int userId, CancellationToken ct = default); + Task HasAnyProgressOnSeriesAsync(int seriesId, int userId, CancellationToken ct = default); + Task> GetUserProgressForSeriesAsync(int seriesId, int userId, CancellationToken ct = default); + Task> GetAllProgress(CancellationToken ct = default); + Task GetLatestProgress(CancellationToken ct = default); + Task GetUserProgressDtoAsync(int chapterId, int userId, CancellationToken ct = default); + Task AnyUserProgressForSeriesAsync(int seriesId, int userId, CancellationToken ct = default); + Task GetHighestFullyReadChapterForSeries(int seriesId, int userId, CancellationToken ct = default); + Task GetHighestFullyReadVolumeForSeries(int seriesId, int userId, CancellationToken ct = default); + Task GetLatestProgressForSeries(int seriesId, int userId, CancellationToken ct = default); + Task GetLatestProgressForVolume(int volumeId, int userId, CancellationToken ct = default); + Task GetLatestProgressForChapter(int chapterId, int userId, CancellationToken ct = default); + Task GetFirstProgressForSeries(int seriesId, int userId, CancellationToken ct = default); + Task GetFirstProgressForUser(int userId, CancellationToken ct = default); + Task UpdateAllProgressThatAreMoreThanChapterPages(CancellationToken ct = default); + Task> GetUserProgressForChapter(int chapterId, int userId = 0, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IAppUserReadingProfileRepository.cs b/Kavita.API/Repositories/IAppUserReadingProfileRepository.cs new file mode 100644 index 000000000..657bfd8ca --- /dev/null +++ b/Kavita.API/Repositories/IAppUserReadingProfileRepository.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Repositories; + +public interface IAppUserReadingProfileRepository +{ + /// + /// Returns the reading profile to use for the given series + /// + /// + /// + /// + /// + /// + /// + /// + Task GetProfileForSeries(int userId, int libraryId, int seriesId, int? activeDeviceId = null, bool skipImplicit = false, CancellationToken ct = default); + + /// + /// Get all profiles assigned to a library + /// + /// + /// + /// + /// + Task> GetProfilesForLibrary(int userId, int libraryId, CancellationToken ct = default); + + /// + /// Return the profile if it belongs to the user + /// + /// + /// + /// + /// + Task GetUserProfile(int userId, int profileId, CancellationToken ct = default); + + /// + /// Returns all reading profiles for the user + /// + /// + /// + /// + /// + Task> GetProfilesForUser(int userId, bool skipImplicit = false, CancellationToken ct = default); + + /// + /// Returns all reading profiles for the user + /// + /// + /// + /// + /// + Task> GetProfilesDtoForUser(int userId, bool skipImplicit = false, CancellationToken ct = default); + + /// + /// Is there a user reading profile with this name (normalized)? + /// + /// + /// + /// + /// + Task IsProfileNameInUse(int userId, string name, CancellationToken ct = default); + + void Add(AppUserReadingProfile readingProfile); + void Update(AppUserReadingProfile readingProfile); + void Remove(AppUserReadingProfile readingProfile); + void RemoveRange(IEnumerable readingProfiles); +} diff --git a/Kavita.API/Repositories/IAppUserSmartFilterRepository.cs b/Kavita.API/Repositories/IAppUserSmartFilterRepository.cs new file mode 100644 index 000000000..6b8049e36 --- /dev/null +++ b/Kavita.API/Repositories/IAppUserSmartFilterRepository.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Dashboard; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Repositories; + +public interface IAppUserSmartFilterRepository +{ + void Update(AppUserSmartFilter filter); + void Attach(AppUserSmartFilter filter); + void Delete(AppUserSmartFilter filter); + Task> GetAllDtosByUserId(int userId, CancellationToken ct = default); + Task> GetPagedDtosByUserIdAsync(int userId, UserParams userParams, CancellationToken ct = default); + Task GetById(int smartFilterId, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IChapterRepository.cs b/Kavita.API/Repositories/IChapterRepository.cs new file mode 100644 index 000000000..dccbb8fff --- /dev/null +++ b/Kavita.API/Repositories/IChapterRepository.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; + +namespace Kavita.API.Repositories; + +[Flags] +public enum ChapterIncludes +{ + None = 1 << 0, + Volumes = 1 << 1, + Files = 1 << 2, + People = 1 << 3, + Genres = 1 << 4, + Tags = 1 << 5, + ExternalReviews = 1 << 6, + ExternalRatings = 1 << 7 +} + +public interface IChapterRepository +{ + void Update(Chapter chapter); + void Remove(Chapter chapter); + void Remove(IList chapters); + Task> GetChaptersByIdsAsync(IList chapterIds, ChapterIncludes includes = ChapterIncludes.None, CancellationToken ct = default); + Task GetChapterInfoDtoAsync(int chapterId, CancellationToken ct = default); + Task GetChapterTotalPagesAsync(int chapterId, CancellationToken ct = default); + Task GetChapterAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files, CancellationToken ct = default); + Task GetChapterDtoAsync(int chapterId, int userId, CancellationToken ct = default); + Task> GetChapterDtoByIdsAsync(IEnumerable chapterIds, int userId, CancellationToken ct = default); + Task GetChapterMetadataDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files, CancellationToken ct = default); + Task> GetFilesForChapterAsync(int chapterId, CancellationToken ct = default); + Task> GetChaptersAsync(int volumeId, ChapterIncludes includes = ChapterIncludes.None, CancellationToken ct = default); + Task> GetChapterDtosAsync(int volumeId, int userId, CancellationToken ct = default); + Task> GetFilesForChaptersAsync(IReadOnlyList chapterIds, CancellationToken ct = default); + Task GetFilesizeForChapterAsync(int chapterId, CancellationToken ct = default); + Task> GetFilesizeForChaptersAsync(IList chapterIds, CancellationToken ct = default); + Task GetChapterCoverImageAsync(int chapterId, CancellationToken ct = default); + Task> GetAllCoverImagesAsync(CancellationToken ct = default); + Task> GetAllChaptersWithCoversInDifferentEncoding(EncodeFormat format, CancellationToken ct = default); + Task> GetCoverImagesForLockedChaptersAsync(CancellationToken ct = default); + IQueryable GetChaptersForSeries(int seriesId, CancellationToken ct = default); + Task> GetAllChaptersForSeries(int seriesId, CancellationToken ct = default); + Task GetAverageUserRating(int chapterId, int userId, CancellationToken ct = default); + Task> GetExternalChapterReviewDtos(int chapterId, CancellationToken ct = default); + Task> GetExternalChapterReview(int chapterId, CancellationToken ct = default); + Task> GetExternalChapterRatingDtos(int chapterId, CancellationToken ct = default); + Task> GetExternalChapterRatings(int chapterId, CancellationToken ct = default); + Task GetCurrentlyReadingChapterAsync(int seriesId, int userId, CancellationToken ct = default); + Task GetFirstChapterForSeriesAsync(int seriesId, int userId, CancellationToken ct = default); + Task GetFirstChapterForVolumeAsync(int volumeId, int userId, CancellationToken ct = default); + Task> GetChapterDtosAsync(IEnumerable chapterIds, int userId, CancellationToken ct = default); + Task GetSeriesIdForChapter(int chapterId, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IClientDeviceRepository.cs b/Kavita.API/Repositories/IClientDeviceRepository.cs new file mode 100644 index 000000000..722176ee3 --- /dev/null +++ b/Kavita.API/Repositories/IClientDeviceRepository.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Repositories; + +public interface IClientDeviceRepository +{ + Task GetClientDeviceById(int id, int userId, CancellationToken cancellationToken = default); + Task GetClientDeviceByClientFingerprint(int userId, string uiFingerprint, CancellationToken cancellationToken); + Task> GetUserDevicesAsync(int userId, bool includeInactive = false, CancellationToken cancellationToken = default); + Task> GetUserDeviceDtosAsync(int userId, bool includeInactive = false, CancellationToken cancellationToken = default); + Task> GetAllUserDeviceDtos(bool includeInactive = false, CancellationToken cancellationToken = default); +} diff --git a/Kavita.API/Repositories/ICollectionTagRepository.cs b/Kavita.API/Repositories/ICollectionTagRepository.cs new file mode 100644 index 000000000..fb9c84e07 --- /dev/null +++ b/Kavita.API/Repositories/ICollectionTagRepository.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Collection; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Repositories; + +[Flags] +public enum CollectionTagIncludes +{ + None = 1 << 0, + SeriesMetadata = 1 << 1, + SeriesMetadataWithSeries = 1 << 2 +} + +[Flags] +public enum CollectionIncludes +{ + None = 1 << 0, + Series = 1 << 1, +} + +public interface ICollectionTagRepository +{ + void Remove(AppUserCollection tag); + void Update(AppUserCollection tag); + Task GetCoverImageAsync(int collectionTagId, CancellationToken ct = default); + Task GetCollectionAsync(int tagId, CollectionIncludes includes = CollectionIncludes.None, CancellationToken ct = default); + Task RemoveCollectionsWithoutSeries(CancellationToken ct = default); + Task GetCollectionDtoAsync(int collectionId, int userId, CancellationToken ct = default); + + Task> GetAllCollectionsAsync(CollectionIncludes includes = CollectionIncludes.None, CancellationToken ct = default); + + /// + /// Returns all of the user's collections with the option of other user's promoted + /// + /// + /// + /// + /// + Task> GetCollectionDtosAsync(int userId, bool includePromoted = false, CancellationToken ct = default); + Task> GetCollectionDtosPagedAsync(int userId, UserParams userParams, bool includePromoted = false, CancellationToken ct = default); + Task> GetCollectionDtosBySeriesAsync(int userId, int seriesId, bool includePromoted = false, CancellationToken ct = default); + + Task> GetAllCoverImagesAsync(CancellationToken ct = default); + Task CollectionExists(string title, int userId, CancellationToken ct = default); + Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, CancellationToken ct = default); + Task> GetRandomCoverImagesAsync(int collectionId, CancellationToken ct = default); + Task> GetCollectionsForUserAsync(int userId, CollectionIncludes includes = CollectionIncludes.None, CancellationToken ct = default); + Task UpdateCollectionAgeRating(AppUserCollection tag, CancellationToken ct = default); + Task> GetCollectionsByIds(IEnumerable tags, CollectionIncludes includes = CollectionIncludes.None, CancellationToken ct = default); + Task> GetAllCollectionsForSyncing(DateTime expirationTime, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IDeviceRepository.cs b/Kavita.API/Repositories/IDeviceRepository.cs new file mode 100644 index 000000000..5b0ad240a --- /dev/null +++ b/Kavita.API/Repositories/IDeviceRepository.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Device.EmailDevice; +using Kavita.Models.Entities; + +namespace Kavita.API.Repositories; + +public interface IDeviceRepository +{ + void Update(Device device); + Task> GetDevicesForUserAsync(int userId, CancellationToken ct = default); + Task GetDeviceById(int deviceId, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IEmailHistoryRepository.cs b/Kavita.API/Repositories/IEmailHistoryRepository.cs new file mode 100644 index 000000000..b2e4d1a29 --- /dev/null +++ b/Kavita.API/Repositories/IEmailHistoryRepository.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Email; + +namespace Kavita.API.Repositories; + +public interface IEmailHistoryRepository +{ + Task> GetEmailDtos(UserParams userParams, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IEpubFontRepository.cs b/Kavita.API/Repositories/IEpubFontRepository.cs new file mode 100644 index 000000000..80cf7bc9d --- /dev/null +++ b/Kavita.API/Repositories/IEpubFontRepository.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Font; +using Kavita.Models.Entities; + +namespace Kavita.API.Repositories; + +public interface IEpubFontRepository +{ + void Add(EpubFont font); + void Remove(EpubFont font); + void Update(EpubFont font); + Task> GetFontDtosAsync(CancellationToken ct = default); + Task GetFontDtoAsync(int fontId, CancellationToken ct = default); + Task GetFontDtoByNameAsync(string name, CancellationToken ct = default); + Task> GetFontsAsync(CancellationToken ct = default); + Task GetFontAsync(int fontId, CancellationToken ct = default); + Task IsFontInUseAsync(int fontId, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IExternalSeriesMetadataRepository.cs b/Kavita.API/Repositories/IExternalSeriesMetadataRepository.cs new file mode 100644 index 000000000..2a5f3168f --- /dev/null +++ b/Kavita.API/Repositories/IExternalSeriesMetadataRepository.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.KavitaPlus.Manage; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Metadata; + +namespace Kavita.API.Repositories; + +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, CancellationToken ct = default); + Task NeedsDataRefresh(int seriesId, CancellationToken ct = default); + Task GetSeriesDetailPlusDto(int seriesId, CancellationToken ct = default); + Task LinkRecommendationsToSeries(Series series, CancellationToken ct = default); + Task IsBlacklistedSeries(int seriesId, CancellationToken ct = default); + Task> GetSeriesThatNeedExternalMetadata(int limit, bool includeStaleData = false, CancellationToken ct = default); + Task> GetAllSeries(ManageMatchFilterDto filter, UserParams userParams, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IGenreRepository.cs b/Kavita.API/Repositories/IGenreRepository.cs new file mode 100644 index 000000000..b0bdf5895 --- /dev/null +++ b/Kavita.API/Repositories/IGenreRepository.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Metadata.Browse; +using Kavita.Models.Entities; + +namespace Kavita.API.Repositories; + +public interface IGenreRepository +{ + void Attach(Genre genre); + void Remove(Genre genre); + Task FindByNameAsync(string genreName, CancellationToken ct = default); + Task> GetAllGenresAsync(CancellationToken ct = default); + Task> GetAllGenresByNamesAsync(IEnumerable normalizedNames, CancellationToken ct = default); + Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false, CancellationToken ct = default); + Task> GetAllGenreDtosForLibrariesAsync(int userId, IList? libraryIds = null, QueryContext context = QueryContext.None, CancellationToken ct = default); + Task GetCountAsync(CancellationToken ct = default); + Task GetRandomGenre(CancellationToken ct = default); + Task GetGenreById(int id, CancellationToken ct = default); + Task> GetAllGenresNotInListAsync(ICollection genreNames, CancellationToken ct = default); + Task> GetBrowseableGenre(int userId, UserParams userParams, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/ILibraryRepository.cs b/Kavita.API/Repositories/ILibraryRepository.cs new file mode 100644 index 000000000..148e0102b --- /dev/null +++ b/Kavita.API/Repositories/ILibraryRepository.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.JumpBar; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; + +namespace Kavita.API.Repositories; + +[Flags] +public enum LibraryIncludes +{ + None = 1 << 0, + Series = 1 << 1, + AppUser = 1 << 2, + Folders = 1 << 3, + FileTypes = 1 << 4, + ExcludePatterns = 1 << 5 +} + +public interface ILibraryRepository +{ + void Add(Library library); + void Update(Library library); + void Delete(Library? library); + Task> GetLibraryDtosAsync(CancellationToken ct = default); + Task GetLibraryDtoByIdAsync(int libraryId, CancellationToken ct = default); + Task GetLiteLibraryDtoByIdAsync(int libraryId, CancellationToken ct = default); + Task LibraryExists(string libraryName, CancellationToken ct = default); + Task GetLibraryForIdAsync(int libraryId, LibraryIncludes includes = LibraryIncludes.None, CancellationToken ct = default); + Task> GetLibraryDtosForUsernameAsync(string userName, CancellationToken ct = default); + Task> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None, bool track = true, CancellationToken ct = default); + Task> GetLibrariesForUserIdAsync(int userId, CancellationToken ct = default); + Task> GetLibraryIdsForUserIdAsync(int userId, QueryContext queryContext = QueryContext.None, CancellationToken ct = default); + Task GetLibraryTypeAsync(int libraryId, CancellationToken ct = default); + Task GetLibraryTypeBySeriesIdAsync(int seriesId, CancellationToken ct = default); + Task> GetLibraryForIdsAsync(IEnumerable libraryIds, LibraryIncludes includes = LibraryIncludes.None, CancellationToken ct = default); + Task GetTotalFiles(CancellationToken ct = default); + IEnumerable GetJumpBarAsync(int libraryId, CancellationToken ct = default); + Task> GetAllAgeRatingsDtosForLibrariesAsync(List libraryIds, CancellationToken ct = default); + Task> GetAllLanguagesForLibrariesAsync(List? libraryIds, CancellationToken ct = default); + IEnumerable GetAllPublicationStatusesDtosForLibrariesAsync(List libraryIds, CancellationToken ct = default); + Task DoAnySeriesFoldersMatch(IEnumerable folders, CancellationToken ct = default); + Task GetLibraryCoverImageAsync(int libraryId, CancellationToken ct = default); + Task> GetAllCoverImagesAsync(CancellationToken ct = default); + Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, CancellationToken ct = default); + Task GetAllowsScrobblingBySeriesId(int seriesId, CancellationToken ct = default); + + Task> GetLibraryTypesBySeriesIdsAsync(IList seriesIds, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IMangaFileRepository.cs b/Kavita.API/Repositories/IMangaFileRepository.cs new file mode 100644 index 000000000..6fbf6b46c --- /dev/null +++ b/Kavita.API/Repositories/IMangaFileRepository.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.Entities; + +namespace Kavita.API.Repositories; + +public interface IMangaFileRepository +{ + void Update(MangaFile file); + Task> GetAllWithMissingExtension(CancellationToken ct = default); + Task GetByKoreaderHash(string hash, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IMediaErrorRepository.cs b/Kavita.API/Repositories/IMediaErrorRepository.cs new file mode 100644 index 000000000..b99b88604 --- /dev/null +++ b/Kavita.API/Repositories/IMediaErrorRepository.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.MediaErrors; +using Kavita.Models.Entities; + +namespace Kavita.API.Repositories; + +public interface IMediaErrorRepository +{ + void Attach(MediaError error); + void Remove(MediaError error); + void Remove(IList errors); + Task Find(string filename, CancellationToken ct = default); + Task> GetAllErrorDtosAsync(CancellationToken ct = default); + Task ExistsAsync(MediaError error, CancellationToken ct = default); + Task DeleteAll(CancellationToken ct = default); + Task> GetAllErrorsAsync(IList comments, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IPersonRepository.cs b/Kavita.API/Repositories/IPersonRepository.cs new file mode 100644 index 000000000..79c62ded1 --- /dev/null +++ b/Kavita.API/Repositories/IPersonRepository.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Metadata.Browse; +using Kavita.Models.DTOs.Metadata.Browse.Requests; +using Kavita.Models.DTOs.Person; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Person; + +namespace Kavita.API.Repositories; + +[Flags] +public enum PersonIncludes +{ + None = 1 << 0, + Aliases = 1 << 1, + ChapterPeople = 1 << 2, + SeriesPeople = 1 << 3, + + All = Aliases | ChapterPeople | SeriesPeople, +} + +public interface IPersonRepository +{ + void Attach(Person person); + void Attach(IEnumerable person); + void Remove(Person person); + void Remove(ChapterPeople person); + void Remove(SeriesMetadataPeople person); + void Update(Person person); + + Task> GetAllPeople(PersonIncludes includes = PersonIncludes.Aliases, CancellationToken ct = default); + Task> GetAllPersonDtosAsync(int userId, PersonIncludes includes = PersonIncludes.None, CancellationToken ct = default); + Task> GetAllPersonDtosByRoleAsync(int userId, PersonRole role, PersonIncludes includes = PersonIncludes.None, CancellationToken ct = default); + Task RemoveAllPeopleNoLongerAssociated(CancellationToken ct = default); + Task> GetAllPeopleDtosForLibrariesAsync(int userId, List? libraryIds = null, PersonIncludes includes = PersonIncludes.None, CancellationToken ct = default); + + Task GetCoverImageAsync(int personId, CancellationToken ct = default); + Task> GetAllCoverImagesAsync(CancellationToken ct = default); + Task GetCoverImageByNameAsync(string name, CancellationToken ct = default); + Task> GetRolesForPersonByName(int personId, int userId, CancellationToken ct = default); + Task> GetBrowsePersonDtos(int userId, BrowsePersonFilterDto filter, UserParams userParams, CancellationToken ct = default); + Task GetPersonById(int personId, PersonIncludes includes = PersonIncludes.None, CancellationToken ct = default); + Task GetPersonDtoByName(string name, int userId, PersonIncludes includes = PersonIncludes.Aliases, CancellationToken ct = default); + + /// + /// Returns a person matched on a normalized name or alias + /// + /// + /// + /// + /// + Task GetPersonByNameOrAliasAsync(string name, PersonIncludes includes = PersonIncludes.Aliases, CancellationToken ct = default); + Task IsNameUnique(string name, CancellationToken ct = default); + + Task> GetSeriesKnownFor(int personId, int userId, CancellationToken ct = default); + Task> GetChaptersForPersonByRole(int personId, int userId, PersonRole role, CancellationToken ct = default); + + /// + /// Returns all people with a matching name, or alias + /// + /// + /// + /// + /// + Task> GetPeopleByNames(List normalizedNames, PersonIncludes includes = PersonIncludes.Aliases, CancellationToken ct = default); + Task GetPersonByAniListId(int aniListId, PersonIncludes includes = PersonIncludes.Aliases, CancellationToken ct = default); + + Task> SearchPeople(string searchQuery, PersonIncludes includes = PersonIncludes.Aliases, CancellationToken ct = default); + + Task AnyAliasExist(string alias, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IReadingListRepository.cs b/Kavita.API/Repositories/IReadingListRepository.cs new file mode 100644 index 000000000..569ad3d90 --- /dev/null +++ b/Kavita.API/Repositories/IReadingListRepository.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; + +namespace Kavita.API.Repositories; + +[Flags] +public enum ReadingListIncludes +{ + None = 1 << 0, + Items = 1 << 1, + ItemChapter = 1 << 2, +} + +public interface IReadingListRepository +{ + void Remove(ReadingListItem item); + void Add(ReadingList list); + void BulkRemove(IEnumerable items); + void Update(ReadingList list); + + Task> GetReadingListDtosForUserAsync(int userId, bool includePromoted, UserParams userParams, bool sortByLastModified = true, CancellationToken ct = default); + Task GetReadingListByIdAsync(int readingListId, ReadingListIncludes includes = ReadingListIncludes.None, CancellationToken ct = default); + Task> GetReadingListItemDtosByIdAsync(int readingListId, int userId, UserParams? userParams = null, CancellationToken ct = default); + Task GetReadingListDtoByIdAsync(int readingListId, int userId, CancellationToken ct = default); + Task GetReadingListDtoByTitleAsync(int userId, string title, CancellationToken ct = default); + Task> GetReadingListItemsByIdAsync(int readingListId, CancellationToken ct = default); + Task> GetReadingListDtosForSeriesAndUserAsync(int userId, int seriesId, + bool includePromoted, CancellationToken ct = default); + Task> GetReadingListDtosForChapterAndUserAsync(int userId, int chapterId, + bool includePromoted, CancellationToken ct = default); + Task Count(CancellationToken ct = default); + Task GetCoverImageAsync(int readingListId, CancellationToken ct = default); + Task> GetRandomCoverImagesAsync(int readingListId, CancellationToken ct = default); + Task> GetAllCoverImagesAsync(CancellationToken ct = default); + Task ReadingListExists(string name, int? readingListId = null, CancellationToken ct = default); + Task ReadingListExistsForUser(string name, int userId, CancellationToken ct = default); + IEnumerable GetReadingListPeopleAsync(int readingListId, PersonRole role, CancellationToken ct = default); + Task GetReadingListAllPeopleAsync(int readingListId, CancellationToken ct = default); + Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, CancellationToken ct = default); + Task RemoveReadingListsWithoutSeries(CancellationToken ct = default); + Task GetReadingListByTitleAsync(string name, int userId, ReadingListIncludes includes = ReadingListIncludes.Items, CancellationToken ct = default); + Task> GetReadingListsByIds(IList ids, ReadingListIncludes includes = ReadingListIncludes.Items, CancellationToken ct = default); + Task> GetReadingListsBySeriesId(int seriesId, ReadingListIncludes includes = ReadingListIncludes.Items, CancellationToken ct = default); + Task GetReadingListInfoAsync(int readingListId, CancellationToken ct = default); + Task AnyUserReadingProgressAsync(int readingListId, int userId, CancellationToken ct = default); + Task GetContinueReadingPoint(int readingListId, int userId, CancellationToken ct = default); + Task GetReadingListItemCountAsync(int readingListId, int userId, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IReadingSessionRepository.cs b/Kavita.API/Repositories/IReadingSessionRepository.cs new file mode 100644 index 000000000..1037b7fed --- /dev/null +++ b/Kavita.API/Repositories/IReadingSessionRepository.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Progress; + +namespace Kavita.API.Repositories; + +public interface IReadingSessionRepository +{ + Task> GetAllReadingSessionAsync(bool isActiveOnly = true, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IScrobbleRepository.cs b/Kavita.API/Repositories/IScrobbleRepository.cs new file mode 100644 index 000000000..32e820e8c --- /dev/null +++ b/Kavita.API/Repositories/IScrobbleRepository.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.Entities.Scrobble; + +namespace Kavita.API.Repositories; + +public interface IScrobbleRepository +{ + void Attach(ScrobbleEvent evt); + void Attach(ScrobbleError error); + void Remove(ScrobbleEvent evt); + void Remove(IEnumerable events); + void Remove(IEnumerable errors); + void Update(ScrobbleEvent evt); + Task> GetByEvent(ScrobbleEventType type, bool isProcessed = false, CancellationToken ct = default); + Task> GetProcessedEvents(int daysAgo, CancellationToken ct = default); + Task Exists(int userId, int seriesId, ScrobbleEventType eventType, CancellationToken ct = default); + Task> GetScrobbleErrors(CancellationToken ct = default); + Task> GetAllScrobbleErrorsForSeries(int seriesId, CancellationToken ct = default); + Task ClearScrobbleErrors(CancellationToken ct = default); + Task HasErrorForSeries(int seriesId, CancellationToken ct = default); + + /// + /// Get all events for a specific user and type + /// + /// + /// + /// + /// If true, only returned not processed events + /// + /// + Task GetEvent(int userId, int seriesId, ScrobbleEventType eventType, bool isNotProcessed = false, CancellationToken ct = default); + Task> GetUserEventsForSeries(int userId, int seriesId, CancellationToken ct = default); + + /// + /// Return the events with given ids, when belonging to the passed user + /// + /// + /// + /// + /// + Task> GetUserEvents(int userId, IList scrobbleEventIds, CancellationToken ct = default); + Task> GetUserEvents(int userId, ScrobbleEventFilter filter, UserParams pagination, CancellationToken ct = default); + Task> GetAllEventsForSeries(int seriesId, CancellationToken ct = default); + Task> GetAllEventsWithSeriesIds(IEnumerable seriesIds, CancellationToken ct = default); + Task> GetEvents(CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/ISeriesMetadataRepository.cs b/Kavita.API/Repositories/ISeriesMetadataRepository.cs new file mode 100644 index 000000000..d5fe50cbc --- /dev/null +++ b/Kavita.API/Repositories/ISeriesMetadataRepository.cs @@ -0,0 +1,8 @@ +using Kavita.Models.Entities.Metadata; + +namespace Kavita.API.Repositories; + +public interface ISeriesMetadataRepository +{ + void Update(SeriesMetadata seriesMetadata); +} diff --git a/Kavita.API/Repositories/ISeriesRepository.cs b/Kavita.API/Repositories/ISeriesRepository.cs new file mode 100644 index 000000000..19972d6ac --- /dev/null +++ b/Kavita.API/Repositories/ISeriesRepository.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Dashboard; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.DTOs.Search; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.User; +using Kavita.Models.Misc; +using Kavita.Models.Parser; + +namespace Kavita.API.Repositories; + +[Flags] +public enum SeriesIncludes +{ + None = 1 << 0, + Volumes = 1 << 1, + /// + /// This will include all necessary includes + /// + Metadata = 1 << 2, + Related = 1 << 3, + Library = 1 << 4, + Chapters = 1 << 5, + ExternalReviews = 1 << 6, + ExternalRatings = 1 << 7, + ExternalRecommendations = 1 << 8, + ExternalMetadata = 1 << 9, + + ExternalData = ExternalMetadata | ExternalReviews | ExternalRatings | ExternalRecommendations, +} + +/// +/// For complex queries, Library has certain restrictions where the library should not be included in results. +/// This enum dictates which field to use for the lookup. +/// +public enum QueryContext +{ + None = 1, + Search = 2, + [Obsolete("Use Dashboard")] + Recommended = 3, + Dashboard = 4, +} + +public interface ISeriesRepository +{ + void Add(Series series); + void Attach(SeriesRelation relation); + void Update(Series series); + void Update(SeriesMetadata seriesMetadata); + void Remove(Series series); + void Remove(IEnumerable series); + Task DoesSeriesNameExistInLibrary(string name, int libraryId, MangaFormat format, CancellationToken ct = default); + + /// + /// Adds user information like progress, ratings, etc + /// + /// + /// + /// Pagination info + /// Filtering/Sorting to apply + /// + /// + Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams, FilterDto filter, CancellationToken ct = default); + + /// + /// Does not add user information like progress, ratings, etc. + /// + /// + /// + /// + /// + /// Includes Files in the Search + /// + /// + Task SearchSeries(int userId, bool isAdmin, IList libraryIds, string searchQuery, bool includeChapterAndFiles = true, CancellationToken ct = default); + Task> GetSeriesForLibraryIdAsync(int libraryId, SeriesIncludes includes = SeriesIncludes.None, CancellationToken ct = default); + Task GetSeriesDtoByIdAsync(int seriesId, int userId, CancellationToken ct = default); + Task GetSeriesByIdAsync(int seriesId, SeriesIncludes includes = SeriesIncludes.Volumes | SeriesIncludes.Metadata, CancellationToken ct = default); + Task> GetSeriesDtoByIdsAsync(IEnumerable seriesIds, AppUser user, CancellationToken ct = default); + Task> GetSeriesByIdsAsync(IList seriesIds, bool fullSeries = true, CancellationToken ct = default); + Task GetChapterIdsForSeriesAsync(IList seriesIds, CancellationToken ct = default); + Task>> GetChapterIdWithSeriesIdForSeriesAsync(int[] seriesIds, CancellationToken ct = default); + Task GetFilesizeForSeriesAsync(int seriesId, CancellationToken ct = default); + Task> GetFilesizeForMultipleSeriesAsync(IList seriesIds, CancellationToken ct = default); + Task GetSeriesCoverImageAsync(int seriesId, CancellationToken ct = default); + Task> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto? filter, CancellationToken ct = default); + Task> GetRecentlyAdded(int libraryId, int userId, UserParams userParams, FilterDto filter, CancellationToken ct = default); + Task> GetRecentlyAddedV2(int userId, UserParams userParams, FilterV2Dto filter, CancellationToken ct = default); + Task GetSeriesMetadata(int seriesId, CancellationToken ct = default); + Task> GetSeriesDtoForCollectionAsync(int collectionId, int userId, UserParams userParams, CancellationToken ct = default); + Task> GetFilesForSeries(int seriesId, CancellationToken ct = default); + Task> GetSeriesDtoForIdsAsync(IEnumerable seriesIds, int userId, CancellationToken ct = default); + Task> GetAllCoverImagesAsync(CancellationToken ct = default); + Task> GetLockedCoverImagesAsync(CancellationToken ct = default); + Task> GetFullSeriesForLibraryIdAsync(int libraryId, UserParams userParams, CancellationToken ct = default); + Task GetFullSeriesForSeriesIdAsync(int seriesId, CancellationToken ct = default); + Task GetChunkInfo(int libraryId = 0, CancellationToken ct = default); + Task> GetRecentlyUpdatedSeries(int userId, UserParams? userParams, CancellationToken ct = default); + Task GetRelatedSeries(int userId, int seriesId, CancellationToken ct = default); + Task> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind, CancellationToken ct = default); + Task> GetQuickReads(int userId, int libraryId, UserParams userParams, CancellationToken ct = default); + Task> GetQuickCatchupReads(int userId, int libraryId, UserParams userParams, CancellationToken ct = default); + Task> GetHighlyRated(int userId, int libraryId, UserParams userParams, CancellationToken ct = default); + Task> GetMoreIn(int userId, int libraryId, int genreId, UserParams userParams, CancellationToken ct = default); + Task> GetRediscover(int userId, int libraryId, UserParams userParams, CancellationToken ct = default); + Task GetSeriesForMangaFile(int mangaFileId, int userId, CancellationToken ct = default); + Task GetSeriesForChapter(int chapterId, int userId, CancellationToken ct = default); + Task> GetWantToReadForUserAsync(int userId, UserParams userParams, FilterDto filter, CancellationToken ct = default); + Task> GetWantToReadForUserV2Async(int userId, UserParams userParams, FilterV2Dto filter, CancellationToken ct = default); + Task> GetWantToReadForUserAsync(int userId, CancellationToken ct = default); + Task IsSeriesInWantToRead(int userId, int seriesId, CancellationToken ct = default); + Task GetSeriesByFolderPath(string folder, SeriesIncludes includes = SeriesIncludes.None, CancellationToken ct = default); + Task GetSeriesThatContainsLowestFolderPath(string path, SeriesIncludes includes = SeriesIncludes.None, CancellationToken ct = default); + Task> GetAllSeriesByNameAsync(IList normalizedNames, + int userId, SeriesIncludes includes = SeriesIncludes.None, CancellationToken ct = default); + Task GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, MangaFormat format, bool withFullIncludes = true, CancellationToken ct = default); + Task GetSeriesByAnyName(IList names, IList formats, + int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None, CancellationToken ct = default); + Task GetSeriesByAnyName(string seriesName, string localizedName, IList formats, int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None, CancellationToken ct = default); + public Task> GetAllSeriesByAnyName(string seriesName, string localizedName, int libraryId, + MangaFormat format, CancellationToken ct = default); + Task> RemoveSeriesNotInList(IList seenSeries, int libraryId, CancellationToken ct = default); + Task>> GetFolderPathMap(int libraryId, CancellationToken ct = default); + Task GetMaxAgeRatingFromSeriesAsync(IEnumerable seriesIds, CancellationToken ct = default); + Task> GetSeriesMetadataForIds(IEnumerable seriesIds, CancellationToken ct = default); + Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, bool customOnly = true, CancellationToken ct = default); + Task GetSeriesDtoByNamesAndMetadataIds(IEnumerable names, LibraryType libraryType, string aniListUrl, string malUrl, CancellationToken ct = default); + Task GetAverageUserRating(int seriesId, int userId, CancellationToken ct = default); + Task RemoveFromOnDeck(int seriesId, int userId, CancellationToken ct = default); + Task ClearOnDeckRemoval(int seriesId, int userId, CancellationToken ct = default); + Task> GetSeriesDtoForLibraryIdV2Async(int userId, UserParams userParams, FilterV2Dto filterDto, QueryContext queryContext = QueryContext.None, CancellationToken ct = default); + Task GetPlusSeriesDto(int seriesId, CancellationToken ct = default); + Task MatchSeries(ExternalSeriesDetailDto externalSeries, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/ISettingsRepository.cs b/Kavita.API/Repositories/ISettingsRepository.cs new file mode 100644 index 000000000..e70bec252 --- /dev/null +++ b/Kavita.API/Repositories/ISettingsRepository.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.MetadataMatching; + +namespace Kavita.API.Repositories; + +public interface ISettingsRepository +{ + void Update(ServerSetting settings); + void Update(MetadataSettings settings); + void Remove(ServerSetting setting); + void RemoveRange(List fieldMappings); + Task GetSettingsDtoAsync(CancellationToken ct = default); + Task GetSettingAsync(ServerSettingKey key, CancellationToken ct = default); + Task> GetSettingsAsync(CancellationToken ct = default); + Task GetExternalSeriesMetadata(int seriesId, CancellationToken ct = default); + Task GetMetadataSettings(CancellationToken ct = default); + Task GetMetadataSettingDto(CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/ISiteThemeRepository.cs b/Kavita.API/Repositories/ISiteThemeRepository.cs new file mode 100644 index 000000000..9a8c6bbc7 --- /dev/null +++ b/Kavita.API/Repositories/ISiteThemeRepository.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Theme; +using Kavita.Models.Entities; + +namespace Kavita.API.Repositories; + +public interface ISiteThemeRepository +{ + void Add(SiteTheme theme); + void Remove(SiteTheme theme); + void Update(SiteTheme siteTheme); + Task> GetThemeDtos(); + Task GetThemeDto(int themeId); + Task GetThemeDtoByName(string themeName); + Task GetDefaultTheme(); + Task> GetThemes(); + Task GetTheme(int themeId); + Task IsThemeInUse(int themeId); +} diff --git a/Kavita.API/Repositories/ITagRepository.cs b/Kavita.API/Repositories/ITagRepository.cs new file mode 100644 index 000000000..0bd075f30 --- /dev/null +++ b/Kavita.API/Repositories/ITagRepository.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Metadata.Browse; +using Kavita.Models.Entities; + +namespace Kavita.API.Repositories; + +public interface ITagRepository +{ + void Attach(Tag tag); + void Remove(Tag tag); + Task> GetAllTagsAsync(CancellationToken ct = default); + Task> GetAllTagsByNameAsync(IEnumerable normalizedNames, CancellationToken ct = default); + Task> GetAllTagDtosAsync(int userId, CancellationToken ct = default); + Task RemoveAllTagNoLongerAssociated(CancellationToken ct = default); + Task> GetAllTagDtosForLibrariesAsync(int userId, IList? libraryIds = null, CancellationToken ct = default); + Task> GetAllTagsNotInListAsync(ICollection tags, CancellationToken ct = default); + Task> GetBrowseableTag(int userId, UserParams userParams, CancellationToken ct = default); +} diff --git a/Kavita.API/Repositories/IUserRepository.cs b/Kavita.API/Repositories/IUserRepository.cs new file mode 100644 index 000000000..d5940aebb --- /dev/null +++ b/Kavita.API/Repositories/IUserRepository.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Account; +using Kavita.Models.DTOs.Dashboard; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.DTOs.KavitaPlus.Account; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.DTOs.SideNav; +using Kavita.Models.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Repositories; + +[Flags] +public enum AppUserIncludes +{ + None = 1, + Progress = 1 << 1, + Bookmarks = 1 << 2, + ReadingLists = 1 << 3, + Ratings = 1 << 4, + UserPreferences = 1 << 5, + WantToRead = 1 << 6, + ReadingListsWithItems = 1 << 7, + Devices = 1 << 8, + ScrobbleHolds = 1 << 9, + SmartFilters = 1 << 10, + DashboardStreams = 1 << 11, + SideNavStreams = 1 << 12, + ExternalSources = 1 << 13, + Collections = 1 << 14, + ChapterRatings = 1 << 15, + AuthKeys = 1 << 16 +} + +public interface IUserRepository +{ + #region Synchronous CRUD + void Add(AppUserAuthKey key); + void Add(AppUserBookmark bookmark); + void Add(AppUser bookmark); + void Update(AppUser user); + void Update(AppUserPreferences preferences); + void Update(AppUserBookmark bookmark); + void Update(AppUserDashboardStream stream); + void Update(AppUserSideNavStream stream); + void Delete(AppUser? user); + void Delete(AppUserAuthKey? key); + void Delete(AppUserBookmark bookmark); + void Delete(IEnumerable streams); + void Delete(AppUserDashboardStream stream); + void Delete(IEnumerable streams); + void Delete(AppUserSideNavStream stream); + #endregion + + #region User Retrieval + Task> GetEmailConfirmedMemberDtosAsync(bool emailConfirmed = true, CancellationToken ct = default); + Task> GetAdminUsersAsync(CancellationToken ct = default); + Task IsUserAdminAsync(AppUser? user, CancellationToken ct = default); + Task> GetRoles(int userId, CancellationToken ct = default); + Task> GetRolesByAuthKey(string? apiKey, CancellationToken ct = default); + Task GetUserDtoByAuthKeyAsync(string authKey, CancellationToken ct = default); + Task GetUserIdByAuthKeyAsync(string authKey, CancellationToken ct = default); + Task GetUserDtoById(int userId, CancellationToken ct = default); + Task GetUserByUsernameAsync(string username, AppUserIncludes includeFlags = AppUserIncludes.None, CancellationToken ct = default); + Task GetUserByIdAsync(int userId, AppUserIncludes includeFlags = AppUserIncludes.None, CancellationToken ct = default); + Task GetUserByAuthKey(string authKey, AppUserIncludes includeFlags = AppUserIncludes.None, CancellationToken ct = default); + Task GetUserIdByUsernameAsync(string username, CancellationToken ct = default); + Task GetUserByEmailAsync(string email, AppUserIncludes includes = AppUserIncludes.None, CancellationToken ct = default); + Task> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None, bool track = true, CancellationToken ct = default); + Task GetUserByConfirmationToken(string token, CancellationToken ct = default); + Task GetDefaultAdminUser(AppUserIncludes includes = AppUserIncludes.None, CancellationToken ct = default); + Task> GetUserTokenInfo(CancellationToken ct = default); + Task GetUserByDeviceEmail(string deviceEmail, CancellationToken ct = default); + Task GetByOidcId(string? oidcId, AppUserIncludes includes = AppUserIncludes.None, CancellationToken ct = default); + Task UpdateUserAsActive(int userId, CancellationToken ct = default); + #endregion + + #region Ratings & Reviews + Task GetUserRatingAsync(int seriesId, int userId, CancellationToken ct = default); + Task GetUserChapterRatingAsync(int userId, int chapterId, CancellationToken ct = default); + Task> GetUserRatingDtosForSeriesAsync(int seriesId, int userId, CancellationToken ct = default); + Task> GetUserRatingDtosForChapterAsync(int chapterId, int userId, CancellationToken ct = default); + Task> GetSeriesWithRatings(int userId, CancellationToken ct = default); + Task> GetSeriesWithReviews(int userId, CancellationToken ct = default); + Task> GetAllReviewsForUser(int userId, int requestingUserId, string? query = null, float? ratingFilter = null, CancellationToken ct = default); + #endregion + + #region Bookmarks + Task> GetBookmarkDtosForSeries(int userId, int seriesId, CancellationToken ct = default); + Task> GetBookmarkDtosForVolume(int userId, int volumeId, CancellationToken ct = default); + Task> GetBookmarkDtosForChapter(int userId, int chapterId, CancellationToken ct = default); + Task> GetAllBookmarkDtos(int userId, FilterV2Dto filter, CancellationToken ct = default); + Task> GetAllBookmarksAsync(CancellationToken ct = default); + Task GetBookmarkForPage(int page, int chapterId, int imageOffset, int userId, CancellationToken ct = default); + Task GetBookmarkAsync(int bookmarkId, CancellationToken ct = default); + Task> GetAllBookmarksByIds(IList bookmarkIds, CancellationToken ct = default); + #endregion + + #region Preferences & Settings + Task GetPreferencesAsync(string username, CancellationToken ct = default); + Task> GetAllPreferencesByThemeAsync(int themeId, CancellationToken ct = default); + Task> GetAllPreferencesByFontAsync(string fontName, CancellationToken ct = default); + Task GetLocale(int userId, CancellationToken ct = default); + Task GetSocialPreferencesForUser(int userId, CancellationToken ct = default); + Task GetPreferencesForUser(int userId, CancellationToken ct = default); + Task GetOpdsPreferences(int userId, CancellationToken ct = default); + #endregion + + #region Permissions + Task HasAccessToLibrary(int libraryId, int userId, CancellationToken ct = default); + Task HasAccessToSeries(int userId, int seriesId, CancellationToken ct = default); + Task HasAccessToVolume(int userId, int volumeId, CancellationToken ct = default); + Task HasAccessToChapter(int userId, int chapterId, CancellationToken ct = default); + Task HasAccessToPerson(int userId, int personId, CancellationToken ct = default); + Task HasAccessToReadingList(int userId, int readingListId, CancellationToken ct = default); + #endregion + + #region Scrobbling & Holds + Task HasHoldOnSeries(int userId, int seriesId, CancellationToken ct = default); + Task> GetHolds(int userId, CancellationToken ct = default); + #endregion + + #region Streams (Dashboard & SideNav) + Task> GetDashboardStreams(int userId, bool visibleOnly = false, CancellationToken ct = default); + Task> GetAllDashboardStreams(CancellationToken ct = default); + Task GetDashboardStream(int streamId, CancellationToken ct = default); + Task> GetDashboardStreamWithFilter(int filterId, CancellationToken ct = default); + Task> GetSideNavStreams(int userId, bool visibleOnly = false, CancellationToken ct = default); + Task GetSideNavStream(int streamId, CancellationToken ct = default); + Task GetSideNavStreamWithUser(int streamId, CancellationToken ct = default); + Task> GetSideNavStreamWithFilter(int filterId, CancellationToken ct = default); + Task> GetSideNavStreamsByLibraryId(int libraryId, CancellationToken ct = default); + Task> GetSideNavStreamWithExternalSource(int externalSourceId, CancellationToken ct = default); + Task> GetDashboardStreamsByIds(IList streamIds, CancellationToken ct = default); + #endregion + + #region Annotations + Task> GetAnnotations(int userId, int chapterId, CancellationToken ct = default); + Task> GetAnnotationsByPage(int userId, int chapterId, int pageNum, CancellationToken ct = default); + Task GetAnnotationDtoById(int userId, int annotationId, CancellationToken ct = default); + Task> GetAnnotationDtosBySeries(int userId, int seriesId, CancellationToken ct = default); + #endregion + + #region Images & Media + Task GetCoverImageAsync(int userId, CancellationToken ct = default); + Task GetPersonCoverImageAsync(int personId, CancellationToken ct = default); + #endregion + + #region Auth Keys + Task> GetAuthKeysForUserId(int userId, CancellationToken ct = default); + Task> GetAllAuthKeysDtosWithExpiration(CancellationToken ct = default); + Task GetAuthKeyById(int authKeyId, CancellationToken ct = default); + Task GetAuthKeyExpiration(string authKey, int userId, CancellationToken ct = default); + #endregion +} diff --git a/Kavita.API/Repositories/IUserTableOfContentRepository.cs b/Kavita.API/Repositories/IUserTableOfContentRepository.cs new file mode 100644 index 000000000..37771a03e --- /dev/null +++ b/Kavita.API/Repositories/IUserTableOfContentRepository.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Repositories; + +public interface IUserTableOfContentRepository +{ + void Attach(AppUserTableOfContent toc); + void Remove(AppUserTableOfContent toc); + Task IsUnique(int userId, int chapterId, int page, string title); + Task> GetPersonalToC(int userId, int chapterId); + Task> GetPersonalToCForPage(int userId, int chapterId, int page); + Task Get(int userId, int chapterId, int pageNum, string title); +} diff --git a/Kavita.API/Repositories/IVolumeRepository.cs b/Kavita.API/Repositories/IVolumeRepository.cs new file mode 100644 index 000000000..b78ff110f --- /dev/null +++ b/Kavita.API/Repositories/IVolumeRepository.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; + +namespace Kavita.API.Repositories; + +[Flags] +public enum VolumeIncludes +{ + None = 1 << 0, + Chapters = 1 << 1, + People = 1 << 2, + Tags = 1 << 3, + /// + /// This will include Chapters by default + /// + Files = 1 << 4 +} + +public interface IVolumeRepository +{ + void Add(Volume volume); + void Update(Volume volume); + void Remove(Volume volume); + void Remove(IList volumes); + Task> GetFilesForVolume(int volumeId, CancellationToken ct = default); + Task GetVolumeCoverImageAsync(int volumeId, CancellationToken ct = default); + Task> GetChapterIdsByVolumeIds(IReadOnlyList volumeIds, CancellationToken ct = default); + Task> GetVolumesDtoAsync(int seriesId, int userId, VolumeIncludes includes = VolumeIncludes.Chapters, CancellationToken ct = default); + Task GetVolumeByIdAsync(int volumeId, VolumeIncludes includes = VolumeIncludes.Files, CancellationToken ct = default); + Task GetVolumeDtoAsync(int volumeId, int userId, CancellationToken ct = default); + Task> GetVolumesForSeriesAsync(IList seriesIds, bool includeChapters = false, CancellationToken ct = default); + Task> GetVolumes(int seriesId, CancellationToken ct = default); + Task> GetVolumesById(IList volumeIds, VolumeIncludes includes = VolumeIncludes.None, CancellationToken ct = default); + Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, CancellationToken ct = default); + Task> GetCoverImagesForLockedVolumesAsync(CancellationToken ct = default); + Task GetFilesizeForVolumeAsync(int volumeId, CancellationToken ct = default); + Task> GetFilesizeForVolumesAsync(IList volumeIds, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/Helpers/ICacheHelper.cs b/Kavita.API/Services/Helpers/ICacheHelper.cs new file mode 100644 index 000000000..f2248ae0a --- /dev/null +++ b/Kavita.API/Services/Helpers/ICacheHelper.cs @@ -0,0 +1,18 @@ +using System; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Interfaces; + +namespace Kavita.API.Services.Helpers; + +public interface ICacheHelper +{ + bool ShouldUpdateCoverImage(string coverPath, MangaFile? firstFile, DateTime chapterCreated, + bool forceUpdate = false, + bool isCoverLocked = false); + + bool CoverImageExists(string path); + + bool IsFileUnmodifiedSinceCreationOrLastScan(IEntityDate chapter, bool forceUpdate, MangaFile? firstFile); + bool HasFileChangedSinceLastScan(DateTime lastScan, bool forceUpdate, MangaFile? firstFile); + +} diff --git a/Kavita.API/Services/IAccountService.cs b/Kavita.API/Services/IAccountService.cs new file mode 100644 index 000000000..76e608fa4 --- /dev/null +++ b/Kavita.API/Services/IAccountService.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.API.Errors; +using Kavita.Common; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Microsoft.AspNetCore.Identity; + +namespace Kavita.API.Services; + +public interface IAccountService +{ + Task> ChangeUserPassword(AppUser user, string newPassword, CancellationToken ct = default); + Task> ValidatePassword(AppUser user, string password, CancellationToken ct = default); + Task> ValidateUsername(string? username, CancellationToken ct = default); + Task> ValidateEmail(string email, CancellationToken ct = default); + Task CanChangeAgeRestriction(AppUser? user, CancellationToken ct = default); + + /// + /// + /// + /// The user who is changing the identity + /// the user being changed + /// the provider being changed to + /// + /// If true, user should not be updated by kavita (anymore) + /// Throws if invalid actions are being performed + Task ChangeIdentityProvider(int actingUserId, AppUser user, IdentityProvider identityProvider, CancellationToken ct = default); + + /// + /// Removes access to all libraries, then grant access to all given libraries or all libraries if the user is admin. + /// Creates side nav streams as well + /// + /// + /// + /// + /// + /// + /// Ensure that the users SideNavStreams are loaded + /// Does NOT commit + Task UpdateLibrariesForUser(AppUser user, IList librariesIds, bool hasAdminRole, CancellationToken ct = default); + Task> UpdateRolesForUser(AppUser user, IList roles, CancellationToken ct = default); + + /// + /// Seeds all information necessary for a new user + /// + /// + /// + /// + Task SeedUser(AppUser user, CancellationToken ct = default); + void AddDefaultStreamsToUser(AppUser user, CancellationToken ct = default); + Task AddDefaultReadingProfileToUser(AppUser user, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IAnnotationService.cs b/Kavita.API/Services/IAnnotationService.cs new file mode 100644 index 000000000..4cc903591 --- /dev/null +++ b/Kavita.API/Services/IAnnotationService.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Reader; + +namespace Kavita.API.Services; + +public interface IAnnotationService +{ + Task CreateAnnotation(int userId, AnnotationDto dto, CancellationToken ct = default); + Task UpdateAnnotation(int userId, AnnotationDto dto, CancellationToken ct = default); + + /// + /// Export all annotations for a user, or optionally specify which annotation exactly + /// + /// + /// + /// + /// + Task ExportAnnotations(int userId, IList? annotationIds = null, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IArchiveService.cs b/Kavita.API/Services/IArchiveService.cs new file mode 100644 index 000000000..8eb5683da --- /dev/null +++ b/Kavita.API/Services/IArchiveService.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.IO.Compression; +using System.Threading.Tasks; +using Kavita.Common; +using Kavita.Models.DTOs.Archive; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; + +namespace Kavita.API.Services; + +public interface IArchiveService +{ + void ExtractArchive(string archivePath, string extractPath); + int GetNumberOfPagesFromArchive(string archivePath); + string GetCoverImage(string archivePath, string fileName, string outputDirectory, EncodeFormat format, CoverImageSize size = CoverImageSize.Default); + bool IsValidArchive(string archivePath); + ComicInfo? GetComicInfo(string archivePath); + ArchiveLibrary CanOpen(string archivePath); + bool ArchiveNeedsFlattening(ZipArchive archive); + /// + /// 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); +} diff --git a/Kavita.API/Services/IAuthKeyService.cs b/Kavita.API/Services/IAuthKeyService.cs new file mode 100644 index 000000000..a5413239c --- /dev/null +++ b/Kavita.API/Services/IAuthKeyService.cs @@ -0,0 +1,19 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Kavita.API.Services; + +public interface IAuthKeyService +{ + Task UpdateLastAccessedAsync(string authKey, CancellationToken ct = default); + + /// + /// Invalidates the cached authentication data for a specific auth key. + /// Call this when a key is rotated or deleted. + /// + /// The actual key value (not the ID) + /// Cancellation token + Task InvalidateAsync(string keyValue, CancellationToken cancellationToken = default); + + string CreateCacheKey(string keyValue); +} diff --git a/Kavita.API/Services/IBackupService.cs b/Kavita.API/Services/IBackupService.cs new file mode 100644 index 000000000..0b71d53e0 --- /dev/null +++ b/Kavita.API/Services/IBackupService.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Kavita.API.Services; + +public interface IBackupService +{ + public const string LogFile = "config/logs/kavita.log"; + + Task BackupDatabase(CancellationToken ct = default); + /// + /// Returns a list of all log files for Kavita + /// + /// If file rolling is enabled. Defaults to True. + /// + IEnumerable GetLogFiles(bool rollFiles = true); +} diff --git a/Kavita.API/Services/IBookService.cs b/Kavita.API/Services/IBookService.cs new file mode 100644 index 000000000..40b819a70 --- /dev/null +++ b/Kavita.API/Services/IBookService.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Models.Parser; +using VersOne.Epub; + +namespace Kavita.API.Services; + +public interface IBookService +{ + int GetNumberOfPages(string filePath); + string GetCoverImage(string fileFilePath, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); + ComicInfo? GetComicInfo(string filePath); + ParserInfo? ParseInfo(string filePath); + + /// + /// Scopes styles to .reading-section and replaces img src to the passed apiBase + /// + /// + /// + /// If the stylesheetHtml contains Import statements, when scoping the filename, scope needs to be wrt filepath. + /// Book Reference, needed for if you expect Import statements + /// + /// + Task ScopeStyles(string stylesheetHtml, string apiBase, string filename, EpubBookRef book, CancellationToken ct = default); + /// + /// Extracts a PDF file's pages as images to a target directory + /// + /// This method relies on Docnet which has explicit patches from Kavita for ARM support. This should only be used with Tachiyomi + /// + /// Where the files will be extracted to. If doesn't exist, will be created. + void ExtractPdfImages(string fileFilePath, string targetDirectory); + Task> GenerateTableOfContents(Chapter chapter, CancellationToken ct = default); + /// + /// This returns a single page within the epub book. All html will be rewritten to be scoped within our reader, + /// all css is scoped, etc. + /// + /// + /// The requested page + /// The chapterId + /// The path to the cached epub file + /// The API base for Kavita, to rewrite urls to so we load though our endpoint + /// + /// + /// + /// Full epub HTML Page, scoped to Kavita's reader + /// All exceptions throw this + Task GetBookPage(int userId, int page, int chapterId, string cachedEpubPath, string baseUrl, List ptocBookmarks, List annotations, CancellationToken ct = default); + Task> CreateKeyToPageMappingAsync(EpubBookRef book, CancellationToken ct = default); + Task?> GetWordCountsPerPage(string bookFilePath, CancellationToken ct = default); + Task GetWordCountBetweenXPaths(string bookFilePath, string startXpath, int startPage, string endXpath, int endPage, CancellationToken ct = default); + Task CopyImageToTempFromBook(int chapterId, BookmarkDto bookmarkDto, string cachedBookPath, CancellationToken ct = default); + Task GetResourceAsync(string bookFilePath, string requestedKey, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IBookmarkService.cs b/Kavita.API/Services/IBookmarkService.cs new file mode 100644 index 000000000..109ef38c6 --- /dev/null +++ b/Kavita.API/Services/IBookmarkService.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Services; + +public interface IBookmarkService +{ + Task DeleteBookmarkFiles(IEnumerable bookmarks, CancellationToken ct = default); + Task BookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto, string imageToBookmark, CancellationToken ct = default); + Task RemoveBookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto, CancellationToken ct = default); + Task> GetBookmarkFilesById(IEnumerable bookmarkIds, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/ICacheService.cs b/Kavita.API/Services/ICacheService.cs new file mode 100644 index 000000000..75fc3df9d --- /dev/null +++ b/Kavita.API/Services/ICacheService.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities; + +namespace Kavita.API.Services; + +public interface ICacheService +{ + /// + /// Ensures the cache is created for the given chapter and if not, will create it. Should be called before any other + /// cache operations (except cleanup). + /// + /// + /// Extracts a PDF into images for a different reading experience + /// + /// Chapter for the passed chapterId. Side-effect from ensuring cache. + Task Ensure(int chapterId, bool extractPdfToImages = false, CancellationToken ct = default); + /// + /// Clears cache directory of all volumes. This can be invoked from deleting a library or a series. + /// + /// Volumes that belong to that library. Assume the library might have been deleted before this invocation. + void CleanupChapters(IEnumerable chapterIds); + void CleanupBookmarks(IEnumerable seriesIds); + string GetCachedPagePath(int chapterId, int page); + string GetCachePath(int chapterId); + string GetBookmarkCachePath(int seriesId); + IEnumerable GetCachedPages(int chapterId); + IEnumerable GetCachedFileDimensions(string cachePath); + string GetCachedBookmarkPagePath(int seriesId, int page); + string GetCachedFile(Chapter chapter); + string GetCachedFile(int chapterId, string firstFilePath); + public void ExtractChapterFiles(string extractPath, IReadOnlyList files, bool extractPdfImages = false); + Task CacheBookmarkForSeries(int userId, int seriesId, CancellationToken ct = default); + void CleanupBookmarkCache(int seriesId); +} diff --git a/Kavita.API/Services/ICleanupService.cs b/Kavita.API/Services/ICleanupService.cs new file mode 100644 index 000000000..888f60f42 --- /dev/null +++ b/Kavita.API/Services/ICleanupService.cs @@ -0,0 +1,29 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Kavita.API.Services; + +public interface ICleanupService +{ + Task Cleanup(CancellationToken ct = default); + Task CleanupDbEntries(CancellationToken ct = default); + Task CleanupCacheAndTempDirectories(CancellationToken ct = default); + void CleanupCacheDirectory(); + Task DeleteSeriesCoverImages(CancellationToken ct = default); + Task DeleteChapterCoverImages(CancellationToken ct = default); + Task DeleteTagCoverImages(CancellationToken ct = default); + Task CleanupBackups(CancellationToken ct = default); + Task CleanupLogs(CancellationToken ct = default); + void CleanupTemp(); + Task EnsureChapterProgressIsCapped(CancellationToken ct = default); + /// + /// Responsible to remove Series from Want To Read when user's have fully read the series and the series has Publication Status of Completed or Cancelled. + /// + /// + Task CleanupWantToRead(CancellationToken ct = default); + + Task ConsolidateProgress(CancellationToken ct = default); + + Task CleanupMediaErrors(CancellationToken ct = default); + +} diff --git a/Kavita.API/Services/IClientDeviceService.cs b/Kavita.API/Services/IClientDeviceService.cs new file mode 100644 index 000000000..014961a40 --- /dev/null +++ b/Kavita.API/Services/IClientDeviceService.cs @@ -0,0 +1,15 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Device.ClientDevice; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Services; + +public interface IClientDeviceService +{ + Task IdentifyOrRegisterDeviceAsync(int userId, ClientInfoData clientInfo, string? uiFingerprint, CancellationToken cancellationToken = default); + Task RenameDeviceAsync(int userId, int deviceId, string newName, CancellationToken ct = default); + Task DeleteDeviceAsync(int userId, int deviceId, CancellationToken ct = default); + Task UpdateFriendlyNameAsync(int userId, UpdateClientDeviceNameDto dto, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IClientInfoAccessor.cs b/Kavita.API/Services/IClientInfoAccessor.cs new file mode 100644 index 000000000..56d07f2b3 --- /dev/null +++ b/Kavita.API/Services/IClientInfoAccessor.cs @@ -0,0 +1,22 @@ +using Kavita.Models.Entities.Progress; + +namespace Kavita.API.Services; + +/// +/// Provides access to client information for the current request. +/// This service captures details about the client making the request including +/// browser info, device type, authentication method, etc. +/// +public interface IClientInfoAccessor +{ + /// + /// Gets the client information for the current request. + /// Returns null if called outside an HTTP request context (e.g., background jobs). + /// + ClientInfoData? Current { get; } + string? CurrentUiFingerprint { get; } + /// + /// Client Device PK + /// + int? CurrentDeviceId { get; } +} diff --git a/Kavita.API/Services/ICollectionTagService.cs b/Kavita.API/Services/ICollectionTagService.cs new file mode 100644 index 000000000..0a25c27b1 --- /dev/null +++ b/Kavita.API/Services/ICollectionTagService.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Collection; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Services; + +public interface ICollectionTagService +{ + Task DeleteTag(int tagId, AppUser user, CancellationToken ct = default); + Task UpdateTag(AppUserCollectionDto dto, int userId, CancellationToken ct = default); + /// + /// Removes series from Collection tag. Will recalculate max age rating. + /// + /// + /// + /// + /// + Task RemoveTagFromSeries(AppUserCollection? tag, IEnumerable seriesIds, CancellationToken ct = default); + + Task GenerateCollectionCoverImage(int collectionId); +} diff --git a/Kavita.API/Services/IDeviceService.cs b/Kavita.API/Services/IDeviceService.cs new file mode 100644 index 000000000..498094b7e --- /dev/null +++ b/Kavita.API/Services/IDeviceService.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Device.EmailDevice; +using Kavita.Models.Entities; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Services; + +public interface IDeviceService +{ + Task Create(CreateEmailDeviceDto dto, AppUser userWithDevices, CancellationToken ct = default); + Task Update(UpdateEmailDeviceDto dto, AppUser userWithDevices, CancellationToken ct = default); + Task Delete(AppUser userWithDevices, int deviceId, CancellationToken ct = default); + Task SendTo(IReadOnlyList chapterIds, int deviceId, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IDeviceTrackingService.cs b/Kavita.API/Services/IDeviceTrackingService.cs new file mode 100644 index 000000000..77a6e5b87 --- /dev/null +++ b/Kavita.API/Services/IDeviceTrackingService.cs @@ -0,0 +1,12 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.Entities.Progress; + +namespace Kavita.API.Services; + +public interface IDeviceTrackingService +{ + Task TrackDeviceAsync(int userId, ClientInfoData clientInfo, string? uiFingerprint, CancellationToken ct); + Task ClearDeviceCacheAsync(int deviceId); + Task ClearUserDeviceCachesAsync(int userId); +} diff --git a/Kavita.API/Services/IDirectoryService.cs b/Kavita.API/Services/IDirectoryService.cs new file mode 100644 index 000000000..324100ac6 --- /dev/null +++ b/Kavita.API/Services/IDirectoryService.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.System; +using Kavita.Models.Entities.Enums; +using Microsoft.Extensions.Logging; + +namespace Kavita.API.Services; + +public interface IDirectoryService +{ + IFileSystem FileSystem { get; } + string CacheDirectory { get; } + string CoverImageDirectory { get; } + string LogDirectory { get; } + string TempDirectory { get; } + string ConfigDirectory { get; } + string SiteThemeDirectory { get; } + string FaviconDirectory { get; } + string LocalizationDirectory { get; } + string CustomizedTemplateDirectory { get; } + string TemplateDirectory { get; } + string PublisherDirectory { get; } + /// + /// Used for caching documents that may need to stay on disk for more than a day + /// + string LongTermCacheDirectory { get; } + /// + /// Original BookmarkDirectory. Only used for resetting directory. Use for actual path. + /// + string BookmarkDirectory { get; } + /// + /// Used for random files needed, like images to check against, list of countries, etc + /// + string AssetsDirectory { get; } + string EpubFontDirectory { get; } + string BackupDirectory { get; } + + /// + /// Lists out top-level folders for a given directory. Filters out System and Hidden folders. + /// + /// Absolute path of directory to scan. + /// List of folder names + IEnumerable ListDirectory(string rootPath); + Task ReadFileAsync(string path); + bool CopyFilesToDirectory(IEnumerable filePaths, string directoryPath, string prepend = ""); + bool CopyFilesToDirectory(IEnumerable filePaths, string directoryPath, IList newFilenames); + bool Exists(string directory); + void CopyFileToDirectory(string fullFilePath, string targetDirectory); + int TraverseTreeParallelForEach(string root, Action action, string searchPattern, ILogger logger); + bool IsDriveMounted(string path); + bool IsDirectoryEmpty(string path); + long GetTotalSize(IEnumerable paths); + void ClearDirectory(string directoryPath); + void ClearAndDeleteDirectory(string directoryPath); + string[] GetFilesWithExtension(string path, string searchPatternExpression = ""); + bool CopyDirectoryToDirectory(string? sourceDirName, string destDirName, string searchPattern = ""); + Dictionary FindHighestDirectoriesFromFiles(IEnumerable libraryFolders, + IList filePaths); + string? FindLowestDirectoriesFromFiles(IList libraryFolders, + IList filePaths); + IEnumerable GetFoldersTillRoot(string rootPath, string fullPath); + IEnumerable GetFiles(string path, string fileNameRegex = "", SearchOption searchOption = SearchOption.TopDirectoryOnly); + bool ExistOrCreate(string directoryPath); + void DeleteFiles(IEnumerable files); + void CopyFile(string sourcePath, string destinationPath, bool overwrite = true); + void RemoveNonImages(string directoryName); + void Flatten(string directoryName); + Task CheckWriteAccess(string directoryName); + IEnumerable GetFilesWithCertainExtensions(string path, + string searchPatternExpression = "", + SearchOption searchOption = SearchOption.TopDirectoryOnly); + IEnumerable GetDirectories(string folderPath); + IEnumerable GetDirectories(string folderPath, GlobMatcher? matcher); + IEnumerable GetAllDirectories(string folderPath, GlobMatcher? matcher = null); + string GetParentDirectoryName(string fileOrFolder); + IList ScanFiles(string folderPath, string fileTypes, GlobMatcher? matcher = null, SearchOption searchOption = SearchOption.AllDirectories); + DateTime GetLastWriteTime(string folderPath); +} diff --git a/Kavita.API/Services/IDownloadService.cs b/Kavita.API/Services/IDownloadService.cs new file mode 100644 index 000000000..f61333bd0 --- /dev/null +++ b/Kavita.API/Services/IDownloadService.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using Kavita.Models.Entities; + +namespace Kavita.API.Services; + +public interface IDownloadService +{ + Tuple GetFirstFileDownload(IEnumerable files); + string GetContentTypeFromFile(string filepath); +} diff --git a/Kavita.API/Services/IEmailService.cs b/Kavita.API/Services/IEmailService.cs new file mode 100644 index 000000000..407f8754f --- /dev/null +++ b/Kavita.API/Services/IEmailService.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Email; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Microsoft.AspNetCore.Http; + +namespace Kavita.API.Services; + +public interface IEmailService +{ + Task SendInviteEmail(ConfirmationEmailDto data); + Task SendForgotPasswordEmail(PasswordResetEmailDto dto); + Task SendFilesToEmail(SendToDto data); + Task SendTestEmail(string adminEmail); + Task SendEmailChangeEmail(ConfirmationEmailDto data); + bool IsValidEmail(string email); + + Task GenerateEmailLink(HttpRequest request, string token, string routePart, string email, + bool withHost = true); + + Task SendTokenExpiredEmail(int userId, ScrobbleProvider provider); + Task SendTokenExpiringSoonEmail(int userId, ScrobbleProvider provider); + Task SendAuthKeyExpiredEmail(int userId, IList keys); + Task SendAuthKeyExpiringSoonEmail(int userId, IList keys); + Task SendKavitaPlusDebug(); +} diff --git a/API/Helpers/Formatting/LocalizedNamingContext.cs b/Kavita.API/Services/IEntityNamingService.cs similarity index 52% rename from API/Helpers/Formatting/LocalizedNamingContext.cs rename to Kavita.API/Services/IEntityNamingService.cs index 7cabd6377..d7957db6c 100644 --- a/API/Helpers/Formatting/LocalizedNamingContext.cs +++ b/Kavita.API/Services/IEntityNamingService.cs @@ -1,11 +1,51 @@ -using System.Threading.Tasks; -using API.DTOs; -using API.DTOs.ReadingLists; -using API.Entities.Enums; -using API.Services; +using System.Threading.Tasks; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.Entities.Enums; -namespace API.Helpers.Formatting; -#nullable enable +namespace Kavita.API.Services; + +/// +/// Provides consistent, testable naming for series, volumes, and chapters across the application. +/// All methods are pure functions with no side effects. +/// +public interface IEntityNamingService +{ + /// + /// Formats a chapter title based on library type and chapter metadata. + /// + string FormatChapterTitle(LibraryType libraryType, ChapterDto chapter, string? chapterLabel = null, string? issueLabel = null, string? bookLabel = null); + + /// + /// Formats a chapter title from raw values. + /// + string FormatChapterTitle(LibraryType libraryType, bool isSpecial, string range, string? title, string? chapterLabel = null, string? issueLabel = null, string? bookLabel = null, bool withHash = true); + + /// + /// Formats a volume name based on library type and volume metadata. + /// + string? FormatVolumeName(LibraryType libraryType, VolumeDto volume, string? volumeLabel = null); + /// + /// Builds a full display title for a chapter within a series/volume context. + /// Used for OPDS feeds, reading lists, etc. + /// + string BuildFullTitle(LibraryType libraryType, SeriesDto series, VolumeDto? volume, ChapterDto chapter, string? volumeLabel = null, string? chapterLabel = null, string? issueLabel = null, string? bookLabel = null); + /// + /// Builds a display title for a chapter within its volume context. + /// Used when series context is not needed (e.g., reading history within a series grouping). + /// + string BuildChapterTitle(LibraryType libraryType, VolumeDto volume, ChapterDto chapter, string? volumeLabel = null, string? chapterLabel = null, string? issueLabel = null, string? bookLabel = null); + /// + /// Formats a reading list item title based on the item's metadata. + /// Handles the unique naming conventions for reading list display. + /// + string FormatReadingListItemTitle(ReadingListItemDto item, string? volumeLabel = null, string? chapterLabel = null, string? issueLabel = null, string? bookLabel = null); + + /// + /// Formats a reading list item title from raw values. + /// + string FormatReadingListItemTitle( LibraryType libraryType, MangaFormat format, string? chapterNumber, string? volumeNumber, string? chapterTitleName, bool isSpecial, string? volumeLabel = null, string? chapterLabel = null, string? issueLabel = null, string? bookLabel = null); +} /// /// Pre-fetched localized labels for entity naming. diff --git a/Kavita.API/Services/IFileService.cs b/Kavita.API/Services/IFileService.cs new file mode 100644 index 000000000..063375144 --- /dev/null +++ b/Kavita.API/Services/IFileService.cs @@ -0,0 +1,12 @@ +using System; +using System.IO.Abstractions; + +namespace Kavita.API.Services; + +public interface IFileService +{ + IFileSystem GetFileSystem(); + bool HasFileBeenModifiedSince(string filePath, DateTime time); + bool Exists(string filePath); + bool ValidateSha(string filepath, string sha); +} diff --git a/Kavita.API/Services/IFontService.cs b/Kavita.API/Services/IFontService.cs new file mode 100644 index 000000000..fa4428f7a --- /dev/null +++ b/Kavita.API/Services/IFontService.cs @@ -0,0 +1,13 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.Entities; + +namespace Kavita.API.Services; + +public interface IFontService +{ + Task CreateFontFromFileAsync(string path, CancellationToken ct = default); + Task Delete(int fontId, CancellationToken ct = default); + Task CreateFontFromUrl(string url, CancellationToken ct = default); + Task IsFontInUse(int fontId, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IImageService.cs b/Kavita.API/Services/IImageService.cs new file mode 100644 index 000000000..c9cf430ab --- /dev/null +++ b/Kavita.API/Services/IImageService.cs @@ -0,0 +1,61 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; + +namespace Kavita.API.Services; + +public interface IImageService +{ + void ExtractImages(string fileFilePath, string targetDirectory, int fileCount = 1); + string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size); + + /// + /// Creates a Thumbnail version of a base64 image + /// + /// base64 encoded image + /// + /// Convert and save as encoding format + /// Width of thumbnail + /// If null, will write to + /// File name with extension of the file. + string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = 320, string? targetDirectory = null); + /// + /// Writes out a thumbnail by stream input + /// + /// + /// + /// + /// + /// + string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); + /// + /// Writes out a thumbnail by file path input + /// + /// + /// + /// + /// + /// + string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); + + /// + /// Converts the passed image to encoding and outputs it in the same directory + /// + /// Full path to the image to convert + /// Where to output the file + /// Encoding Format + /// + /// File of written encoded image + Task ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat, CancellationToken ct = default); + + /// + /// Performs I/O to determine if the file is a valid Image + /// + /// + /// + /// + Task IsImage(string filePath, CancellationToken ct = default); + void UpdateColorScape(IHasCoverImage entity); +} diff --git a/Kavita.API/Services/IKoreaderService.cs b/Kavita.API/Services/IKoreaderService.cs new file mode 100644 index 000000000..2ec3f6ba6 --- /dev/null +++ b/Kavita.API/Services/IKoreaderService.cs @@ -0,0 +1,11 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Koreader; + +namespace Kavita.API.Services; + +public interface IKoreaderService +{ + Task SaveProgress(KoreaderBookDto koreaderBookDto, int userId, CancellationToken ct = default); + Task GetProgress(string bookHash, int userId, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/ILocalizationService.cs b/Kavita.API/Services/ILocalizationService.cs new file mode 100644 index 000000000..40a9a8bdf --- /dev/null +++ b/Kavita.API/Services/ILocalizationService.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Kavita.Models.DTOs; + +namespace Kavita.API.Services; + +public interface ILocalizationService +{ + Task Get(string locale, string key, params object[] args); + Task Translate(int userId, string key, params object[] args); + IEnumerable GetLocales(); +} diff --git a/Kavita.API/Services/ILoggingService.cs b/Kavita.API/Services/ILoggingService.cs new file mode 100644 index 000000000..cbd105de0 --- /dev/null +++ b/Kavita.API/Services/ILoggingService.cs @@ -0,0 +1,6 @@ +namespace Kavita.API.Services; + +public interface ILoggingService +{ + void SwitchLogLevel(string level); +} diff --git a/Kavita.API/Services/IMediaConversionService.cs b/Kavita.API/Services/IMediaConversionService.cs new file mode 100644 index 000000000..bbca39626 --- /dev/null +++ b/Kavita.API/Services/IMediaConversionService.cs @@ -0,0 +1,19 @@ +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Kavita.Models.Entities.Enums; + +namespace Kavita.API.Services; + +public interface IMediaConversionService +{ + [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)] + Task ConvertAllBookmarkToEncoding(CancellationToken ct = default); + [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)] + Task ConvertAllCoversToEncoding(CancellationToken ct = default); + [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)] + Task ConvertAllManagedMediaToEncodingFormat(CancellationToken ct = default); + + Task SaveAsEncodingFormat(string imageDirectory, string filename, string targetFolder, + EncodeFormat encodeFormat); +} diff --git a/Kavita.API/Services/IMediaErrorService.cs b/Kavita.API/Services/IMediaErrorService.cs new file mode 100644 index 000000000..3e65b2448 --- /dev/null +++ b/Kavita.API/Services/IMediaErrorService.cs @@ -0,0 +1,14 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.Entities.Enums; + +namespace Kavita.API.Services; + +public interface IMediaErrorService +{ + void ReportMediaIssue(string filename, MediaErrorProducer producer, string errorMessage, string details); + void ReportMediaIssue(string filename, MediaErrorProducer producer, string errorMessage, Exception ex); + Task ReportMediaIssueAsync(string filename, MediaErrorProducer producer, string errorMessage, string details, CancellationToken ct = default); + Task ReportMediaIssueAsync(string filename, MediaErrorProducer producer, string errorMessage, Exception ex, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IMetadataService.cs b/Kavita.API/Services/IMetadataService.cs new file mode 100644 index 000000000..3ccdbbfa4 --- /dev/null +++ b/Kavita.API/Services/IMetadataService.cs @@ -0,0 +1,35 @@ +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; + +namespace Kavita.API.Services; + +public interface IMetadataService +{ + /// + /// Recalculates cover images for all entities in a library. + /// + /// + /// + /// + /// + [DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)] + [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] + Task GenerateCoversForLibrary(int libraryId, bool forceUpdate = false, bool forceColorScape = false, CancellationToken ct = default); + + /// + /// Performs a forced refresh of cover images just for a series, and it's nested entities + /// + /// + /// + /// + /// Overrides any cache logic and forces execution + /// + /// + Task GenerateCoversForSeries(ServerSettingDto serverSetting, int libraryId, int seriesId, bool forceUpdate = true, bool forceColorScape = true, CancellationToken ct = default); + Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceUpdate = false, bool forceColorScape = true, CancellationToken ct = default); + Task RemoveAbandonedMetadataKeys(CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IOidcService.cs b/Kavita.API/Services/IOidcService.cs new file mode 100644 index 000000000..a7e2a8803 --- /dev/null +++ b/Kavita.API/Services/IOidcService.cs @@ -0,0 +1,38 @@ +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common; +using Kavita.Models.Entities.User; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Http; + +namespace Kavita.API.Services; + +public interface IOidcService +{ + /// + /// Returns the user authenticated with OpenID Connect + /// + /// + /// + /// + /// + /// if any requirements aren't met + Task LoginOrCreate(HttpRequest request, ClaimsPrincipal principal, CancellationToken ct = default); + + /// + /// Refresh the token inside the cookie when it's close to expiring. And sync the user + /// + /// + /// + /// + /// If the token is refreshed successfully, updates the last active time of the suer + Task RefreshCookieToken(CookieValidatePrincipalContext ctx, CancellationToken ct = default); + + /// + /// Remove from all users + /// + /// + /// + Task ClearOidcIds(CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IOpdsService.cs b/Kavita.API/Services/IOpdsService.cs new file mode 100644 index 000000000..40c7bc999 --- /dev/null +++ b/Kavita.API/Services/IOpdsService.cs @@ -0,0 +1,32 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.OPDS; +using Kavita.Models.DTOs.OPDS.Requests; + +namespace Kavita.API.Services; + +public interface IOpdsService +{ + Task GetCatalogue(OpdsCatalogueRequest request, CancellationToken ct = default); + Task GetSmartFilters(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default); + Task GetLibraries(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default); + Task GetWantToRead(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default); + Task GetCollections(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default); + Task GetReadingLists(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default); + Task GetRecentlyAdded(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default); + Task GetRecentlyUpdated(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default); + Task GetOnDeck(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default); + + Task GetMoreInGenre(OpdsItemsFromEntityIdRequest request, CancellationToken ct = default); + Task GetSeriesFromSmartFilter(OpdsItemsFromEntityIdRequest request, CancellationToken ct = default); + Task GetSeriesFromCollection(OpdsItemsFromEntityIdRequest request, CancellationToken ct = default); + Task GetSeriesFromLibrary(OpdsItemsFromEntityIdRequest request, CancellationToken ct = default); + Task GetReadingListItems(OpdsItemsFromEntityIdRequest request, CancellationToken ct = default); + Task GetSeriesDetail(OpdsItemsFromEntityIdRequest request, CancellationToken ct = default); + Task GetItemsFromVolume(OpdsItemsFromCompoundEntityIdsRequest request, CancellationToken ct = default); + Task GetItemsFromChapter(OpdsItemsFromCompoundEntityIdsRequest request, CancellationToken ct = default); + + Task Search(OpdsSearchRequest request, CancellationToken ct = default); + + string SerializeXml(Feed? feed); +} diff --git a/Kavita.API/Services/IPersonService.cs b/Kavita.API/Services/IPersonService.cs new file mode 100644 index 000000000..66e7f2547 --- /dev/null +++ b/Kavita.API/Services/IPersonService.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.Entities.Person; + +namespace Kavita.API.Services; + +public interface IPersonService +{ + /// + /// Adds src as an alias to dst, this is a destructive operation + /// + /// Merged person + /// Remaining person + /// + /// The entities passed as arguments **must** include all relations + /// + Task MergePeopleAsync(Person src, Person dst, CancellationToken ct = default); + + /// + /// Adds the alias to the person, requires that the aliases are not shared with anyone else + /// + /// This method does NOT commit changes + /// + /// + /// + /// + Task UpdatePersonAliasesAsync(Person person, IList aliases, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IRatingService.cs b/Kavita.API/Services/IRatingService.cs new file mode 100644 index 000000000..df8046df8 --- /dev/null +++ b/Kavita.API/Services/IRatingService.cs @@ -0,0 +1,27 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Services; + +public interface IRatingService +{ + /// + /// Updates the users' rating for a given series + /// + /// Should include ratings + /// + /// + /// + Task UpdateSeriesRating(AppUser user, UpdateRatingDto updateRatingDto, CancellationToken ct = default); + + /// + /// Updates the users' rating for a given chapter + /// + /// Should include ratings + /// chapterId must be set + /// + /// + Task UpdateChapterRating(AppUser user, UpdateRatingDto updateRatingDto, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IReadingItemService.cs b/Kavita.API/Services/IReadingItemService.cs new file mode 100644 index 000000000..2fa6c7f80 --- /dev/null +++ b/Kavita.API/Services/IReadingItemService.cs @@ -0,0 +1,12 @@ +using Kavita.Models.Entities.Enums; +using Kavita.Models.Parser; + +namespace Kavita.API.Services; + +public interface IReadingItemService +{ + int GetNumberOfPages(string filePath, MangaFormat format); + string GetCoverImage(string filePath, string fileName, MangaFormat format, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); + void Extract(string fileFilePath, string targetDirectory, MangaFormat format, int imageCount = 1); + ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata); +} diff --git a/Kavita.API/Services/ISeriesService.cs b/Kavita.API/Services/ISeriesService.cs new file mode 100644 index 000000000..68f98fcf6 --- /dev/null +++ b/Kavita.API/Services/ISeriesService.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.DTOs.SeriesDetail; + +namespace Kavita.API.Services; + +public interface ISeriesService +{ + Task GetSeriesDetail(int seriesId, int userId, CancellationToken ct = default); + Task UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto, CancellationToken ct = default); + Task DeleteMultipleSeries(IList seriesIds, CancellationToken ct = default); + Task UpdateRelatedSeries(UpdateRelatedSeriesDto dto, CancellationToken ct = default); + Task GetRelatedSeries(int userId, int seriesId, CancellationToken ct = default); + Task GetEstimatedChapterCreationDate(int seriesId, int userId, CancellationToken ct = default); + Task> GetCurrentlyReading(int userId, int requestingUserId, UserParams userParams, CancellationToken ct = default); + Task> GetProfilePrivacyStatements(int userId, int requestingUserId, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/ISettingsService.cs b/Kavita.API/Services/ISettingsService.cs new file mode 100644 index 000000000..3c42f3f55 --- /dev/null +++ b/Kavita.API/Services/ISettingsService.cs @@ -0,0 +1,31 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Settings; + +namespace Kavita.API.Services; + +public interface ISettingsService +{ + Task UpdateMetadataSettings(MetadataSettingsDto dto, CancellationToken ct = default); + + /// + /// Update , , , + /// with data from the given dto. + /// + /// + /// + /// + /// + Task ImportFieldMappings(FieldMappingsDto dto, ImportSettingsDto settings, CancellationToken ct = default); + Task UpdateSettings(ServerSettingDto updateSettingsDto, CancellationToken ct = default); + + /// + /// Check if the server can reach the authority at the given uri + /// + /// + /// + /// + Task IsValidAuthority(string authority, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IStatisticService.cs b/Kavita.API/Services/IStatisticService.cs new file mode 100644 index 000000000..74a925a16 --- /dev/null +++ b/Kavita.API/Services/IStatisticService.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.DTOs.Statistics; +using Kavita.Models.DTOs.Stats; +using Kavita.Models.DTOs.Stats.V3.ClientDevice; +using Kavita.Models.Entities.Enums; + +namespace Kavita.API.Services; + +public interface IStatisticService +{ + Task GetServerStatistics(CancellationToken ct = default); + Task GetUserReadStatistics(int userId, IList libraryIds, CancellationToken ct = default); + Task>> GetYearCount(CancellationToken ct = default); + Task>> GetTopYears(CancellationToken ct = default); + Task> GetPopularDecades(CancellationToken ct = default); + Task>> GetPopularLibraries(CancellationToken ct = default); + Task>> GetPopularSeries(CancellationToken ct = default); + Task>> GetPopularReadingList(int take = 5, CancellationToken ct = default); + Task>> GetPopularGenres(CancellationToken ct = default); + Task>> GetPopularTags(CancellationToken ct = default); + Task>> GetPopularPerson(PersonRole role, CancellationToken ct = default); + Task>> GetPublicationCount(CancellationToken ct = default); + Task>> GetMangaFormatCount(CancellationToken ct = default); + Task GetFileBreakdown(CancellationToken ct = default); + Task> GetTopUsers(int days, CancellationToken ct = default); + Task> GetReadingHistory(int userId, CancellationToken ct = default); + Task>> ReadCountByDay(int userId = 0, int days = 0, CancellationToken ct = default); + Task>> ReadCounts(StatsFilterDto filter, int userId = 0, CancellationToken ct = default); + Task>> GetDayBreakdown(int userId = 0, CancellationToken ct = default); + Task>> GetPagesReadCountByYear(int userId = 0, CancellationToken ct = default); + Task>> GetWordsReadCountByYear(int userId = 0, CancellationToken ct = default); + Task UpdateServerStatistics(CancellationToken ct = default); + Task> GetFilesByExtension(string fileExtension, CancellationToken ct = default); + Task GetClientTypeBreakdown(DateTime fromDateUtc, CancellationToken ct = default); + Task>> GetDeviceTypeCounts(DateTime fromDateUtc, CancellationToken ct = default); + Task GetReadingActivityGraphData(StatsFilterDto filter, int userId, int year, int requestingUserId, CancellationToken ct = default); + Task GetReadingPaceForUser(StatsFilterDto filter, int userId, int year, bool booksOnly, int requestingUserId, CancellationToken ct = default); + Task> GetGenreBreakdownForUser(StatsFilterDto filter, int userId, int requestingUserId, CancellationToken ct = default); + Task> GetTagBreakdownForUser(StatsFilterDto filter, int userId, int requestingUserId, CancellationToken ct = default); + Task GetPageSpreadForUser(StatsFilterDto filter, int userId, int requestingUserId, CancellationToken ct = default); + Task GetWordSpreadForUser(StatsFilterDto filter, int userId, int requestingUserId, CancellationToken ct = default); + Task>> GetReadsPerMonth(StatsFilterDto filter, int userId, int requestingUserId, CancellationToken ct = default); + Task> GetMostReadAuthors(StatsFilterDto filter, int userId, int requestingUserId, CancellationToken ct = default); + Task GetTotalReads(int userId, int requestingUserId, CancellationToken ct = default); + Task GetTimeReadingByHour(StatsFilterDto filter, int userId, int requestingUserId, CancellationToken ct = default); + Task GetUserStatBar(StatsFilterDto filter, int userId, int requestingUserId, CancellationToken ct = default); + Task> GetMostActiveUsers(StatsFilterDto filter, CancellationToken ct = default); + Task>> GetFilesAddedOverTime(CancellationToken ct = default); + Task> GetReadingHistoryItems(StatsFilterDto filter, UserParams userParams, int userId, int requestingUserId, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IStatsService.cs b/Kavita.API/Services/IStatsService.cs new file mode 100644 index 000000000..2f8d8ce56 --- /dev/null +++ b/Kavita.API/Services/IStatsService.cs @@ -0,0 +1,12 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Stats; + +namespace Kavita.API.Services; + +public interface IStatsService +{ + Task Send(CancellationToken ct = default); + Task GetServerInfoSlim(CancellationToken ct = default); + Task SendCancellation(CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IStreamService.cs b/Kavita.API/Services/IStreamService.cs new file mode 100644 index 000000000..385906ccf --- /dev/null +++ b/Kavita.API/Services/IStreamService.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Dashboard; +using Kavita.Models.DTOs.SideNav; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Services; + +/// +/// For SideNavStream and DashboardStream manipulation +/// +public interface IStreamService +{ + Task> GetDashboardStreams(int userId, bool visibleOnly = true, CancellationToken ct = default); + Task> GetSidenavStreams(int userId, bool visibleOnly = true, CancellationToken ct = default); + Task> GetExternalSources(int userId, CancellationToken ct = default); + Task CreateDashboardStreamFromSmartFilter(int userId, int smartFilterId, CancellationToken ct = default); + Task UpdateDashboardStream(int userId, DashboardStreamDto dto, CancellationToken ct = default); + Task UpdateDashboardStreamPosition(int userId, UpdateStreamPositionDto dto, CancellationToken ct = default); + Task UpdateSideNavStreamBulk(int userId, BulkUpdateSideNavStreamVisibilityDto dto, CancellationToken ct = default); + Task CreateSideNavStreamFromSmartFilter(int userId, int smartFilterId, CancellationToken ct = default); + Task CreateSideNavStreamFromExternalSource(int userId, int externalSourceId, CancellationToken ct = default); + Task UpdateSideNavStream(int userId, SideNavStreamDto dto, CancellationToken ct = default); + Task UpdateSideNavStreamPosition(int userId, UpdateStreamPositionDto dto, CancellationToken ct = default); + Task CreateExternalSource(int userId, ExternalSourceDto dto, CancellationToken ct = default); + Task UpdateExternalSource(int userId, ExternalSourceDto dto, CancellationToken ct = default); + Task DeleteExternalSource(int userId, int externalSourceId, CancellationToken ct = default); + Task DeleteSideNavSmartFilterStream(int userId, int sideNavStreamId, CancellationToken ct = default); + Task DeleteDashboardSmartFilterStream(int userId, int dashboardStreamId, CancellationToken ct = default); + Task RenameSmartFilterStreams(AppUserSmartFilter smartFilter, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/ITachiyomiService.cs b/Kavita.API/Services/ITachiyomiService.cs new file mode 100644 index 000000000..d9a279801 --- /dev/null +++ b/Kavita.API/Services/ITachiyomiService.cs @@ -0,0 +1,30 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Services; + +public interface ITachiyomiService +{ + /// + /// Gets the latest chapter/volume read. + /// + /// + /// + /// + /// Due to how Tachiyomi works we need a hack to properly return both chapters and volumes. + /// If its a chapter, return the chapterDto as is. + /// If it's a volume, the volume number gets returned in the 'Number' attribute of a chapterDto encoded. + /// The volume number gets divided by 10,000 because that's how Tachiyomi interprets volumes + Task GetLatestChapter(int seriesId, int userId, CancellationToken ct = default); + /// + /// Marks every chapter and volume that is sorted below the passed number as Read. This will not mark any specials as read. + /// Passed number will also be marked as read + /// + /// + /// + /// Can also be a Tachiyomi encoded volume number + /// + Task MarkChaptersUntilAsRead(AppUser userWithProgress, int seriesId, float chapterNumber, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/ITaskScheduler.cs b/Kavita.API/Services/ITaskScheduler.cs new file mode 100644 index 000000000..3fd9d95e1 --- /dev/null +++ b/Kavita.API/Services/ITaskScheduler.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Kavita.API.Services; + +public interface ITaskScheduler +{ + Task ScheduleTasks(CancellationToken cancellationToken = default); + Task ScheduleStatsTasks(CancellationToken cancellationToken = default); + void ScheduleUpdaterTasks(); + Task ScheduleKavitaPlusTasks(CancellationToken cancellationToken = default); + void ScanFolder(string folderPath, string originalPath, TimeSpan delay); + void ScanFolder(string folderPath, bool abortOnNoSeriesMatch = false); + Task ScanLibrary(int libraryId, bool force = false); + Task ScanLibraries(bool force = false); + void CleanupChapters(int[] chapterIds); + void RefreshMetadata(int libraryId, bool forceUpdate = true, bool forceColorscape = true); + Task RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false, bool forceColorscape = false); + Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = false); + void AnalyzeFilesForSeries(int libraryId, int seriesId, bool forceUpdate = false); + void CancelStatsTasks(); + Task RunStatCollection(); + void ConvertAllCoversToEncoding(); + Task CleanupDbEntries(); + Task CheckForUpdate(CancellationToken cancellationToken = default); +} diff --git a/Kavita.API/Services/IThemeService.cs b/Kavita.API/Services/IThemeService.cs new file mode 100644 index 000000000..2db945551 --- /dev/null +++ b/Kavita.API/Services/IThemeService.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Theme; +using Kavita.Models.Entities; + +namespace Kavita.API.Services; + +public interface IThemeService +{ + Task GetContent(int themeId, CancellationToken ct = default); + Task UpdateDefault(int themeId, CancellationToken ct = default); + + /// + /// Browse theme repo for themes to download + /// + /// + /// + Task> GetDownloadableThemes(CancellationToken ct = default); + + Task DownloadRepoTheme(DownloadableSiteThemeDto dto, CancellationToken ct = default); + Task DeleteTheme(int siteThemeId, CancellationToken ct = default); + Task CreateThemeFromFile(string tempFile, string username, CancellationToken ct = default); + Task SyncThemes(CancellationToken ct = default); +} diff --git a/Kavita.API/Services/ITokenService.cs b/Kavita.API/Services/ITokenService.cs new file mode 100644 index 000000000..1f09ea75a --- /dev/null +++ b/Kavita.API/Services/ITokenService.cs @@ -0,0 +1,14 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Account; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Services; + +public interface ITokenService +{ + Task CreateToken(AppUser user, CancellationToken ct = default); + Task ValidateRefreshToken(TokenRequestDto request, CancellationToken ct = default); + Task CreateRefreshToken(AppUser user, CancellationToken ct = default); + Task GetJwtFromUser(AppUser user, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/IVersionUpdaterService.cs b/Kavita.API/Services/IVersionUpdaterService.cs new file mode 100644 index 000000000..5a5839512 --- /dev/null +++ b/Kavita.API/Services/IVersionUpdaterService.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Update; + +namespace Kavita.API.Services; + +public interface IVersionUpdaterService +{ + Task CheckForUpdate(CancellationToken ct = default); + Task PushUpdate(UpdateNotificationDto update, CancellationToken ct = default); + Task> GetAllReleases(int count = 0, CancellationToken ct = default); + Task GetNumberOfReleasesBehind(bool stableOnly = false, CancellationToken ct = default); + void BustGithubCache(CancellationToken ct = default); +} diff --git a/Kavita.API/Services/Metadata/ICoverDbService.cs b/Kavita.API/Services/Metadata/ICoverDbService.cs new file mode 100644 index 000000000..c7f649c1c --- /dev/null +++ b/Kavita.API/Services/Metadata/ICoverDbService.cs @@ -0,0 +1,21 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Person; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Services.Metadata; + +public interface ICoverDbService +{ + Task DownloadFaviconAsync(string url, EncodeFormat encodeFormat, CancellationToken ct = default); + Task DownloadPublisherImageAsync(string publisherName, EncodeFormat encodeFormat, CancellationToken ct = default); + Task DownloadPersonImageAsync(Person person, EncodeFormat encodeFormat, CancellationToken ct = default); + Task DownloadPersonImageAsync(Person person, EncodeFormat encodeFormat, string url, CancellationToken ct = default); + Task SetPersonCoverByUrl(Person person, string url, bool fromBase64 = true, bool checkNoImagePlaceholder = false, bool chooseBetterImage = true, CancellationToken ct = default); + Task SetSeriesCoverByUrl(Series series, string url, bool fromBase64 = true, bool chooseBetterImage = false, CancellationToken ct = default); + Task SetChapterCoverByUrl(Chapter chapter, string url, bool fromBase64 = true, bool chooseBetterImage = false, CancellationToken ct = default); + Task SetUserCoverByUrl(int userId, string url, bool fromBase64 = true, bool chooseBetterImage = false, CancellationToken ct = default); + Task SetUserCoverByUrl(AppUser user, string url, bool fromBase64 = true, bool chooseBetterImage = false, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/Metadata/IWordCountAnalyzerService.cs b/Kavita.API/Services/Metadata/IWordCountAnalyzerService.cs new file mode 100644 index 000000000..6c991dafe --- /dev/null +++ b/Kavita.API/Services/Metadata/IWordCountAnalyzerService.cs @@ -0,0 +1,13 @@ +using System.Threading; +using System.Threading.Tasks; +using Hangfire; + +namespace Kavita.API.Services.Metadata; + +public interface IWordCountAnalyzerService +{ + [DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)] + [AutomaticRetry(Attempts = 2, OnAttemptsExceeded = AttemptsExceededAction.Delete)] + Task ScanLibrary(int libraryId, bool forceUpdate = false, CancellationToken ct = default); + Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = true, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/Plus/IExternalMetadataService.cs b/Kavita.API/Services/Plus/IExternalMetadataService.cs new file mode 100644 index 000000000..6b25ff789 --- /dev/null +++ b/Kavita.API/Services/Plus/IExternalMetadataService.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common; +using Kavita.Models.DTOs.Collection; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Metadata.Matching; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.Entities.Enums; + +namespace Kavita.API.Services.Plus; + +public interface IExternalMetadataService +{ + public static readonly HashSet NonEligibleLibraryTypes = [LibraryType.Comic, LibraryType.Book, LibraryType.Image]; + + /// + /// Retrieves Metadata about a Recommended External Series + /// + /// + /// + /// + /// + /// + /// + Task GetExternalSeriesDetail(int? aniListId, long? malId, int? seriesId, CancellationToken ct = default); + + /// + /// Returns Series Detail data from Kavita+ - Review, Recs, Ratings + /// + /// + /// + /// + /// + Task GetSeriesDetailPlus(int seriesId, LibraryType libraryType, CancellationToken ct = default); + /// + /// 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 25 records. The goal is to slowly build out/refresh the data + /// + Task FetchExternalDataTask(CancellationToken ct = default); + + /// + /// 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. + /// + /// + /// + /// + /// If the fetch was made + Task FetchSeriesMetadata(int seriesId, LibraryType libraryType, CancellationToken ct = default); + + Task> GetStacksForUser(int userId, CancellationToken ct = default); + + /// + /// Returns the match results for a Series from UI Flow + /// + /// + /// Will extract alternative names like Localized name, year will send as ReleaseYear but fallback to Comic Vine syntax if applicable + /// + /// + /// + /// + Task> MatchSeries(MatchSeriesDto dto, CancellationToken ct = default); + + /// + /// This will override any sort of matching that was done prior and force it to be what the user Selected + /// + /// + /// + /// + /// + /// + Task FixSeriesMatch(int seriesId, int? aniListId, long? malId, int? cbrId, CancellationToken ct = default); + + /// + /// Sets a series to Don't Match and removes all previously cached + /// + /// + /// + /// + Task UpdateSeriesDontMatch(int seriesId, bool dontMatch, CancellationToken ct = default); + + /// + /// Given external metadata from Kavita+, write as much as possible to the Kavita series as possible + /// + /// + /// + /// + /// + Task WriteExternalMetadataToSeries(ExternalSeriesDetailDto externalMetadata, int seriesId, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/Plus/IKavitaPlusApiService.cs b/Kavita.API/Services/Plus/IKavitaPlusApiService.cs new file mode 100644 index 000000000..39ba753ca --- /dev/null +++ b/Kavita.API/Services/Plus/IKavitaPlusApiService.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.Collection; +using Kavita.Models.DTOs.KavitaPlus.ExternalMetadata; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Metadata.Matching; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.Entities.Enums; + +namespace Kavita.API.Services.Plus; + +/// +/// All Http requests to K+ should be contained in this service, the service will not handle any errors. +/// This is expected from the caller. +/// +public interface IKavitaPlusApiService +{ + Task HasTokenExpired(string license, string token, ScrobbleProvider provider, CancellationToken ct = default); + Task GetRateLimit(string license, string token, CancellationToken ct = default); + Task PostScrobbleUpdate(ScrobbleDto data, string license, CancellationToken ct = default); + Task> GetMalStacks(string malUsername, string license, CancellationToken ct = default); + Task> MatchSeries(MatchSeriesRequestDto request, CancellationToken ct = default); + Task GetSeriesDetail(PlusSeriesRequestDto request, CancellationToken ct = default); + Task GetSeriesDetailById(ExternalMetadataIdsDto request, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/Plus/ILicenseService.cs b/Kavita.API/Services/Plus/ILicenseService.cs new file mode 100644 index 000000000..bb2033209 --- /dev/null +++ b/Kavita.API/Services/Plus/ILicenseService.cs @@ -0,0 +1,17 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.KavitaPlus.License; + +namespace Kavita.API.Services.Plus; + +public interface ILicenseService +{ + //Task ValidateLicenseStatus(); + Task RemoveLicense(CancellationToken ct = default); + Task AddLicense(string license, string email, string? discordId, CancellationToken ct = default); + Task HasActiveLicense(bool forceCheck = false, CancellationToken ct = default); + Task HasActiveSubscription(string? license, CancellationToken ct = default); + Task ResetLicense(string license, string email, CancellationToken ct = default); + Task GetLicenseInfo(bool forceCheck = false, CancellationToken ct = default); + Task ResendWelcomeEmail(CancellationToken ct = default); +} diff --git a/Kavita.API/Services/Plus/IScrobblingService.cs b/Kavita.API/Services/Plus/IScrobblingService.cs new file mode 100644 index 000000000..71867312a --- /dev/null +++ b/Kavita.API/Services/Plus/IScrobblingService.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; + +namespace Kavita.API.Services.Plus; + +public interface IScrobblingService +{ + /// + /// An automated job that will run against all user's tokens and validate if they are still active + /// + /// + /// This service can validate without license check as the task which calls will be guarded + /// + Task CheckExternalAccessTokens(CancellationToken ct = default); + + /// + /// Checks if the token has expired with , if it has double checks with K+, + /// otherwise return false. + /// + /// + /// + /// + /// + /// Returns true if there is no license present + Task HasTokenExpired(int userId, ScrobbleProvider provider, CancellationToken ct = default); + + /// + /// Create, or update a non-processed, event, for the given series + /// + /// + /// + /// + /// + /// + Task ScrobbleRatingUpdate(int userId, int seriesId, float rating, CancellationToken ct = default); + + /// + /// NOP, until hardcover support has been worked out + /// + /// + /// + /// + /// + /// + /// + Task ScrobbleReviewUpdate(int userId, int seriesId, string? reviewTitle, string reviewBody, CancellationToken ct = default); + + /// + /// Create, or update a non-processed, event, for the given series + /// + /// + /// + /// + /// + Task ScrobbleReadingUpdate(int userId, int seriesId, CancellationToken ct = default); + + /// + /// Creates an or for + /// the given series + /// + /// + /// + /// + /// + /// + /// Only the result of both WantToRead types is send to K+ + Task ScrobbleWantToReadUpdate(int userId, int seriesId, bool onWantToRead, CancellationToken ct = default); + + /// + /// Removed all processed events that are at least 7 days old + /// + /// + /// + [DisableConcurrentExecution(60 * 60 * 60)] + [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] + public Task ClearProcessedEvents(CancellationToken ct = default); + + /// + /// Makes K+ requests for all non-processed events until rate limits are reached + /// + /// + /// + [DisableConcurrentExecution(60 * 60 * 60)] + [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] + Task ProcessUpdatesSinceLastSync(CancellationToken ct = default); + + Task CreateEventsFromExistingHistory(int userId = 0, CancellationToken ct = default); + Task CreateEventsFromExistingHistoryForSeries(int seriesId, CancellationToken ct = default); + Task ClearEventsForSeries(int userId, int seriesId, CancellationToken ct = default); +} + +public static class ScrobblingHelper +{ + public const string AniListWeblinkWebsite = "https://anilist.co/manga/"; + public const string MalWeblinkWebsite = "https://myanimelist.net/manga/"; + public const string MalStaffWebsite = "https://myanimelist.net/people/"; + public const string MalCharacterWebsite = "https://myanimelist.net/character/"; + public const string GoogleBooksWeblinkWebsite = "https://books.google.com/books?id="; + public const string MangaDexWeblinkWebsite = "https://mangadex.org/title/"; + public const string AniListStaffWebsite = "https://anilist.co/staff/"; + public const string AniListCharacterWebsite = "https://anilist.co/character/"; + public const string HardcoverStaffWebsite = "https://hardcover.app/authors/"; + + private static readonly Dictionary WeblinkExtractionMap = new() + { + {AniListWeblinkWebsite, 0}, + {MalWeblinkWebsite, 0}, + {GoogleBooksWeblinkWebsite, 0}, + {MangaDexWeblinkWebsite, 0}, + {AniListStaffWebsite, 0}, + {AniListCharacterWebsite, 0}, + }; + + private static bool IsAniListReviewValid(string reviewTitle, string reviewBody) + { + return string.IsNullOrEmpty(reviewTitle) || string.IsNullOrEmpty(reviewBody) || (reviewTitle.Length < 2200 || + reviewTitle.Length > 120 || + reviewTitle.Length < 20); + } + + public static long? GetMalId(Series series) + { + var malId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite); + return malId ?? series.ExternalSeriesMetadata?.MalId; + } + + public static long? GetMalId(string weblinks) + { + return ExtractId(weblinks, MalWeblinkWebsite); + } + + public static int? GetAniListId(Series seriesWithExternalMetadata) + { + var aniListId = ExtractId(seriesWithExternalMetadata.Metadata.WebLinks, AniListWeblinkWebsite); + return aniListId ?? seriesWithExternalMetadata.ExternalSeriesMetadata?.AniListId; + } + + public static int? GetAniListId(string weblinks) + { + return ExtractId(weblinks, AniListWeblinkWebsite); + } + + /// + /// Extract an Id from a given weblink + /// + /// + /// + /// + public static T? ExtractId(string webLinks, string website) + { + var index = WeblinkExtractionMap[website]; + foreach (var webLink in webLinks.Split(',')) + { + if (!webLink.StartsWith(website)) continue; + + var tokens = webLink.Split(website)[1].Split('/'); + var value = tokens[index]; + + if (typeof(T) == typeof(int?)) + { + if (int.TryParse(value, CultureInfo.InvariantCulture, out var intValue)) return (T)(object)intValue; + } + else if (typeof(T) == typeof(int)) + { + if (int.TryParse(value, CultureInfo.InvariantCulture, out var intValue)) return (T)(object)intValue; + + return default; + } + else if (typeof(T) == typeof(long?)) + { + if (long.TryParse(value, CultureInfo.InvariantCulture, out var longValue)) return (T)(object)longValue; + } + else if (typeof(T) == typeof(string)) + { + return (T)(object)value; + } + } + + return default; + } + + /// + /// Generate a URL from a given ID and website + /// + /// Type of the ID (e.g., int, long, string) + /// The ID to embed in the URL + /// The base website URL + /// The generated URL or null if the website is not supported + public static string? GenerateUrl(T id, string website) + { + if (!WeblinkExtractionMap.ContainsKey(website)) + { + return null; // Unsupported website + } + + if (Equals(id, default(T))) + { + throw new ArgumentNullException(nameof(id), "ID cannot be null."); + } + + // Ensure the type of the ID matches supported types + if (typeof(T) == typeof(int) || typeof(T) == typeof(long) || typeof(T) == typeof(string)) + { + return $"{website}{id}"; + } + + throw new ArgumentException("Unsupported ID type. Supported types are int, long, and string.", nameof(id)); + } + + public static string CreateUrl(string url, long? id) + { + return id is null or 0 ? string.Empty : $"{url}{id}/"; + } + +} diff --git a/Kavita.API/Services/Plus/ISmartCollectionSyncService.cs b/Kavita.API/Services/Plus/ISmartCollectionSyncService.cs new file mode 100644 index 000000000..85ce8351b --- /dev/null +++ b/Kavita.API/Services/Plus/ISmartCollectionSyncService.cs @@ -0,0 +1,25 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Kavita.API.Services.Plus; + +/// +/// Responsible to synchronize Collection series from non-Kavita sources +/// +public interface ISmartCollectionSyncService +{ + /// + /// Synchronize all collections + /// + /// + /// + Task Sync(CancellationToken ct = default); + + /// + /// Synchronize a collection + /// + /// + /// + /// + Task Sync(int collectionId, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/Plus/IWantToReadSyncService.cs b/Kavita.API/Services/Plus/IWantToReadSyncService.cs new file mode 100644 index 000000000..8c394fa5c --- /dev/null +++ b/Kavita.API/Services/Plus/IWantToReadSyncService.cs @@ -0,0 +1,9 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Kavita.API.Services.Plus; + +public interface IWantToReadSyncService +{ + Task Sync(CancellationToken ct = default); +} diff --git a/Kavita.API/Services/Reading/IReaderService.cs b/Kavita.API/Services/Reading/IReaderService.cs new file mode 100644 index 000000000..a59c5e30e --- /dev/null +++ b/Kavita.API/Services/Reading/IReaderService.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Services.Reading; + +public interface IReaderService +{ + public const float MinWordsPerHour = 10260F; + public const float MaxWordsPerHour = 30000F; + public const float MinPagesPerMinute = 3.33F; + public const float MaxPagesPerMinute = 2.75F; + public const float AvgWordsPerHour = (MaxWordsPerHour + MinWordsPerHour) / 2F; + public const float AvgPagesPerMinute = (MaxPagesPerMinute + MinPagesPerMinute) / 2F; //3.04 + + Task MarkSeriesAsRead(AppUser user, int seriesId); + Task MarkSeriesAsUnread(AppUser user, int seriesId); + Task MarkChaptersAsRead(AppUser user, int seriesId, IList chapters); + Task MarkChaptersAsUnread(AppUser user, int seriesId, IList chapters); + Task SaveReadingProgress(ProgressDto progressDto, int userId); + int CapPageToChapter(Chapter chapter, int page); + Task GetNextChapterIdAsync(int seriesId, int volumeId, int currentChapterId, int userId); + Task GetPrevChapterIdAsync(int seriesId, int volumeId, int currentChapterId, int userId); + Task GetContinuePoint(int seriesId, int userId); + Task MarkChaptersUntilAsRead(AppUser user, int seriesId, float chapterNumber); + Task MarkVolumesUntilAsRead(AppUser user, int seriesId, int volumeNumber); + IDictionary GetPairs(IEnumerable dimensions); + Task GetThumbnail(Chapter chapter, int pageNum, IEnumerable cachedImages); + Task CheckSeriesForReRead(int userId, int seriesId, int libraryId); + Task CheckVolumeForReRead(int userId, int volumeId, int seriesId, int libraryId); + Task CheckChapterForReRead(int userId, int chapterId, int seriesId, int libraryId); +} diff --git a/Kavita.API/Services/Reading/IReadingHistoryService.cs b/Kavita.API/Services/Reading/IReadingHistoryService.cs new file mode 100644 index 000000000..52aa77453 --- /dev/null +++ b/Kavita.API/Services/Reading/IReadingHistoryService.cs @@ -0,0 +1,9 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Kavita.API.Services.Reading; + +public interface IReadingHistoryService +{ + Task AggregateYesterdaysActivity(CancellationToken ct = default); +} diff --git a/Kavita.API/Services/Reading/IReadingListService.cs b/Kavita.API/Services/Reading/IReadingListService.cs new file mode 100644 index 000000000..32a4864c4 --- /dev/null +++ b/Kavita.API/Services/Reading/IReadingListService.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.DTOs.ReadingLists.CBL; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; + +namespace Kavita.API.Services.Reading; + +public interface IReadingListService +{ + Task CreateReadingListForUser(AppUser userWithReadingList, string title); + Task UpdateReadingList(ReadingList readingList, UpdateReadingListDto dto); + Task RemoveFullyReadItems(int readingListId, AppUser user); + Task UpdateReadingListItemPosition(UpdateReadingListPosition dto); + Task DeleteReadingListItem(UpdateReadingListPosition dto); + Task UserHasReadingListAccess(int readingListId, string username); + Task DeleteReadingList(int readingListId, AppUser user); + Task CalculateReadingListAgeRating(ReadingList readingList); + Task AddChaptersToReadingList(int seriesId, IList chapterIds, + ReadingList readingList); + + Task ValidateCblFile(int userId, CblReadingList cblReading, bool useComicLibraryMatching = false); + Task CreateReadingListFromCbl(int userId, CblReadingList cblReading, bool dryRun = false, bool useComicLibraryMatching = false); + Task CalculateStartAndEndDates(ReadingList readingListWithItems); + /// + /// This is expected to be called from ProcessSeries and has the Full Series present. Will generate on the default admin user. + /// + /// + /// + /// + Task CreateReadingListsFromSeries(Series series, Library library); + + Task CreateReadingListsFromSeries(int libraryId, int seriesId); + Task GenerateReadingListCoverImage(int readingListId); + /// + /// Check, and update if needed, all reading lists' AgeRating who contain the passed series + /// + /// The series whose age rating is being updated + /// The new (uncommited) age rating of the series + /// + /// This method does not commit changes + Task UpdateReadingListAgeRatingForSeries(int seriesId, AgeRating ageRating); + + Task> GetReadingListItems(int readingListId, int userId, UserParams? userParams = null); + Task GetContinueReadingPoint(int readingListId, int userId); +} diff --git a/Kavita.API/Services/Reading/IReadingProfileService.cs b/Kavita.API/Services/Reading/IReadingProfileService.cs new file mode 100644 index 000000000..eda139aac --- /dev/null +++ b/Kavita.API/Services/Reading/IReadingProfileService.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Kavita.Common; +using Kavita.Models.DTOs; +using Kavita.Models.Entities.Enums; + +namespace Kavita.API.Services.Reading; + +public interface IReadingProfileService +{ + /// + /// Returns the ReadingProfile that should be applied to the given series, walks up the tree. + /// Series (Implicit) -> Series (User) -> Library (User) -> Default + /// + /// + /// + /// + /// + /// + /// + Task GetReadingProfileDtoForSeries(int userId, int libraryId, int seriesId, int? activeDeviceId, bool skipImplicit = false); + + /// + /// Creates a new reading profile for a user. Name must be unique per user + /// + /// + /// + /// + Task CreateReadingProfile(int userId, UserReadingProfileDto dto); + /// + /// Given an implicit profile, promotes it to a profile of kind , then removes + /// all links to the series this implicit profile was created for from other reading profiles (if the device id matches + /// if given) + /// + /// + /// + /// + /// + Task PromoteImplicitProfile(int userId, int profileId, int? activeDeviceId); + + /// + /// Updates the implicit reading profile for a series, creates one if none exists + /// + /// + /// + /// + /// + /// + /// + Task UpdateImplicitReadingProfile(int userId, int libraryId, int seriesId, UserReadingProfileDto dto, int? activeDeviceId); + + /// + /// Updates the non-implicit reading profile for the given series, and removes implicit profiles + /// + /// + /// + /// + /// + /// + /// + Task UpdateParent(int userId, int libraryId, int seriesId, UserReadingProfileDto dto, int? activeDeviceId); + + /// + /// Updates a given reading profile for a user + /// + /// + /// + /// + /// Does not update connected series and libraries + Task UpdateReadingProfile(int userId, UserReadingProfileDto dto); + + /// + /// Deletes a given profile for a user + /// + /// + /// + /// + /// + /// The default profile for the user cannot be deleted + Task DeleteReadingProfile(int userId, int profileId); + + /// + /// Binds the reading profile to the series, and remove the implicit RP from the series if it exists + /// + /// + /// + /// + /// + Task SetSeriesProfiles(int userId, List profileIds, int seriesId); + + /// + /// Binds the reading profile to many series, and remove the implicit RP from the series if it exists + /// + /// + /// + /// + /// + Task BulkSetSeriesProfiles(int userId, List profileIds, List seriesIds); + + /// + /// Remove all reading profiles bound to the series + /// + /// + /// + /// + Task ClearSeriesProfile(int userId, int seriesId); + + /// + /// Bind the reading profile to the library + /// + /// + /// + /// + /// + Task SetLibraryProfiles(int userId, List profileIds, int libraryId); + + /// + /// Remove the reading profile bound to the library, if it exists + /// + /// + /// + /// + Task ClearLibraryProfile(int userId, int libraryId); + + /// + /// Returns the all bound Reading Profile to a Library + /// + /// + /// + /// + Task> GetReadingProfileDtosForLibrary(int userId, int libraryId); + + /// + /// Returns the all bound Reading Profile to a Series + /// + /// + /// + /// + Task> GetReadingProfileDtosForSeries(int userId, int seriesId); + + /// + /// Set the assigned devices for the given reading profile. Then removes all duplicate links, ensuring each series + /// and library only has one profile per device + /// + /// + /// + /// + /// + Task SetProfileDevices(int userId, int profileId, List deviceIds); + + /// + /// Remove device ids from all profiles, does **NOT** commit + /// + /// + /// + /// + Task RemoveDeviceLinks(int userId, int deviceId); +} diff --git a/Kavita.API/Services/Reading/IReadingSessionService.cs b/Kavita.API/Services/Reading/IReadingSessionService.cs new file mode 100644 index 000000000..64bf8fd7c --- /dev/null +++ b/Kavita.API/Services/Reading/IReadingSessionService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities.Progress; + +namespace Kavita.API.Services.Reading; + +public interface IReadingSessionService +{ + Task UpdateProgress(int userId, ProgressDto progressDto, ClientInfoData? clientInfo, int? deviceId); +} diff --git a/Kavita.API/Services/Scanner/ILibraryWatcher.cs b/Kavita.API/Services/Scanner/ILibraryWatcher.cs new file mode 100644 index 000000000..df7b19401 --- /dev/null +++ b/Kavita.API/Services/Scanner/ILibraryWatcher.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; + +namespace Kavita.API.Services.Scanner; + +public interface ILibraryWatcher +{ + /// + /// Start watching all library folders + /// + /// + Task StartWatching(); + /// + /// Stop watching all folders + /// + void StopWatching(); + /// + /// Essentially stops then starts watching. Useful if there is a change in folders or libraries + /// + /// + Task RestartWatching(); +} diff --git a/Kavita.API/Services/Scanner/IProcessSeries.cs b/Kavita.API/Services/Scanner/IProcessSeries.cs new file mode 100644 index 000000000..2f6b4232c --- /dev/null +++ b/Kavita.API/Services/Scanner/IProcessSeries.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.Entities; +using Kavita.Models.Parser; + +namespace Kavita.API.Services.Scanner; + +public sealed record ProcessSeriesArgs +{ + public required Library Library { get; init; } + public required int TotalToProcess { get; init; } + public required int LeftToProcess { get; init; } + public bool ForceUpdate { get; init; } = false; +} + +public interface IProcessSeries +{ + Task ProcessSeriesAsync(MetadataSettingsDto settings, IList parsedInfos, ProcessSeriesArgs args); +} diff --git a/Kavita.API/Services/Scanner/IScannerService.cs b/Kavita.API/Services/Scanner/IScannerService.cs new file mode 100644 index 000000000..f9604b6b0 --- /dev/null +++ b/Kavita.API/Services/Scanner/IScannerService.cs @@ -0,0 +1,34 @@ +using System.Threading.Tasks; +using Hangfire; +using Kavita.Common.Constants; +using Kavita.Models.Constants; + +namespace Kavita.API.Services.Scanner; + +public interface IScannerService +{ + /// + /// Given a library id, scans folders for said library. Parses files and generates DB updates. Will overwrite + /// cover images if forceUpdate is true. + /// + /// Library to scan against + /// Don't perform optimization checks, defaults to false + [Queue(TaskSchedulerConstants.ScanQueue)] + [DisableConcurrentExecution(60 * 60 * 60)] + [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] + Task ScanLibrary(int libraryId, bool forceUpdate = false, bool isSingleScan = true); + + [Queue(TaskSchedulerConstants.ScanQueue)] + [DisableConcurrentExecution(60 * 60 * 60)] + [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] + Task ScanLibraries(bool forceUpdate = false); + + [Queue(TaskSchedulerConstants.ScanQueue)] + [DisableConcurrentExecution(60 * 60 * 60)] + [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] + Task ScanSeries(int seriesId, bool bypassFolderOptimizationChecks = true); + + Task ScanFolder(string folder, string originalPath, bool abortOnNoSeriesMatch = false); + Task AnalyzeFiles(); + +} diff --git a/Kavita.API/Services/SignalR/IEventHub.cs b/Kavita.API/Services/SignalR/IEventHub.cs new file mode 100644 index 000000000..fb6c1e4f8 --- /dev/null +++ b/Kavita.API/Services/SignalR/IEventHub.cs @@ -0,0 +1,14 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.Models.DTOs.SignalR; + +namespace Kavita.API.Services.SignalR; + +/// +/// Responsible for ushering events to the UI and allowing simple DI hook to send data +/// +public interface IEventHub +{ + Task SendMessageAsync(string method, SignalRMessage message, bool onlyAdmins = true, CancellationToken ct = default); + Task SendMessageToAsync(string method, SignalRMessage message, int userId, CancellationToken ct = default); +} diff --git a/Kavita.API/Services/SignalR/IPresenceTracker.cs b/Kavita.API/Services/SignalR/IPresenceTracker.cs new file mode 100644 index 000000000..be8e0cec1 --- /dev/null +++ b/Kavita.API/Services/SignalR/IPresenceTracker.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Kavita.API.Services.SignalR; + +public interface IPresenceTracker +{ + Task UserConnected(int userId, string connectionId); + Task UserDisconnected(int userId, string connectionId); + Task GetOnlineAdminIds(); + /// + /// Returns ids for users that are not admin + /// + /// + Task GetOnlineUserIds(); + Task> GetConnectionsForUser(int userId); +} diff --git a/Kavita.API/Store/IUserContext.cs b/Kavita.API/Store/IUserContext.cs new file mode 100644 index 000000000..8d0099f56 --- /dev/null +++ b/Kavita.API/Store/IUserContext.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using Kavita.Models.Entities.Progress; + +namespace Kavita.API.Store; + +public interface IUserContext +{ + /// + /// Gets the current authenticated user's ID. + /// Returns null if user is not authenticated or on [AllowAnonymous] endpoint. + /// + int? GetUserId(); + + /// + /// Gets the current authenticated user's ID. + /// Throws KavitaException if user is not authenticated. + /// + int GetUserIdOrThrow(); + + /// + /// Gets the current authenticated user's username. + /// Returns null if user is not authenticated. + /// + /// Warning! Username's can contain .. and /, do not use folders or filenames explicitly with the Username + string? GetUsername(); + /// + /// The Roles associated with the Authenticated user + /// + IReadOnlyList Roles { get; } + /// + /// Returns true if the current user is authenticated. + /// + bool IsAuthenticated { get; } + /// + /// Gets the authentication method used (JWT, Auth Key, OIDC). + /// + AuthenticationType GetAuthenticationType(); + + + bool HasRole(string role); + bool HasAnyRole(params string[] roles); + bool HasAllRoles(params string[] roles); +} diff --git a/API.Benchmark/ArchiveServiceBenchmark.cs b/Kavita.Benchmark/ArchiveServiceBenchmark.cs similarity index 97% rename from API.Benchmark/ArchiveServiceBenchmark.cs rename to Kavita.Benchmark/ArchiveServiceBenchmark.cs index 0d13623c2..2eb2307b4 100644 --- a/API.Benchmark/ArchiveServiceBenchmark.cs +++ b/Kavita.Benchmark/ArchiveServiceBenchmark.cs @@ -1,17 +1,16 @@ -using System; -using System.IO; -using System.IO.Abstractions; -using Microsoft.Extensions.Logging.Abstractions; -using API.Services; +using System.IO.Abstractions; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Order; +using Kavita.API.Services; +using Kavita.Services; +using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Processing; -namespace API.Benchmark; +namespace Kavita.Benchmark; [StopOnFirstError] [MemoryDiagnoser] diff --git a/API.Benchmark/CleanTitleBenchmark.cs b/Kavita.Benchmark/CleanTitleBenchmark.cs similarity index 63% rename from API.Benchmark/CleanTitleBenchmark.cs rename to Kavita.Benchmark/CleanTitleBenchmark.cs index 96c57a466..120c76c80 100644 --- a/API.Benchmark/CleanTitleBenchmark.cs +++ b/Kavita.Benchmark/CleanTitleBenchmark.cs @@ -1,8 +1,7 @@ -using System.Collections.Generic; -using System.IO; -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; +using Kavita.Services.Scanner; -namespace API.Benchmark; +namespace Kavita.Benchmark; [MemoryDiagnoser] public class CleanTitleBenchmarks @@ -17,7 +16,7 @@ public class CleanTitleBenchmarks { foreach (var name in _names) { - Services.Tasks.Scanner.Parser.Parser.CleanTitle(name, true); + Parser.CleanTitle(name, true); } } } diff --git a/API.Benchmark/Data/AesopsFables.epub b/Kavita.Benchmark/Data/AesopsFables.epub similarity index 100% rename from API.Benchmark/Data/AesopsFables.epub rename to Kavita.Benchmark/Data/AesopsFables.epub diff --git a/API.Benchmark/Data/Comics.txt b/Kavita.Benchmark/Data/Comics.txt similarity index 100% rename from API.Benchmark/Data/Comics.txt rename to Kavita.Benchmark/Data/Comics.txt diff --git a/API.Benchmark/Data/SeriesNamesForNormalization.txt b/Kavita.Benchmark/Data/SeriesNamesForNormalization.txt similarity index 100% rename from API.Benchmark/Data/SeriesNamesForNormalization.txt rename to Kavita.Benchmark/Data/SeriesNamesForNormalization.txt diff --git a/Kavita.Benchmark/Kavita.Benchmark.csproj b/Kavita.Benchmark/Kavita.Benchmark.csproj new file mode 100644 index 000000000..4afe15da2 --- /dev/null +++ b/Kavita.Benchmark/Kavita.Benchmark.csproj @@ -0,0 +1,44 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + PreserveNewest + + + + + + Always + + + + + Data + Always + + + + + PreserveNewest + + + + + + + + + diff --git a/API.Benchmark/KoreaderHashBenchmark.cs b/Kavita.Benchmark/KoreaderHashBenchmark.cs similarity index 91% rename from API.Benchmark/KoreaderHashBenchmark.cs rename to Kavita.Benchmark/KoreaderHashBenchmark.cs index c0abfd2ad..ffd94adae 100644 --- a/API.Benchmark/KoreaderHashBenchmark.cs +++ b/Kavita.Benchmark/KoreaderHashBenchmark.cs @@ -1,10 +1,9 @@ -using API.Helpers.Builders; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Order; -using System; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Builders; -namespace API.Benchmark +namespace Kavita.Benchmark { [StopOnFirstError] [MemoryDiagnoser] diff --git a/API.Benchmark/ParserBenchmarks.cs b/Kavita.Benchmark/ParserBenchmarks.cs similarity index 94% rename from API.Benchmark/ParserBenchmarks.cs rename to Kavita.Benchmark/ParserBenchmarks.cs index 0dabc560b..4e2d28110 100644 --- a/API.Benchmark/ParserBenchmarks.cs +++ b/Kavita.Benchmark/ParserBenchmarks.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Order; -namespace API.Benchmark; +namespace Kavita.Benchmark; [MemoryDiagnoser] [Orderer(SummaryOrderPolicy.FastestToSlowest)] diff --git a/API.Benchmark/Program.cs b/Kavita.Benchmark/Program.cs similarity index 93% rename from API.Benchmark/Program.cs rename to Kavita.Benchmark/Program.cs index 76ed97c70..a8f15c3a0 100644 --- a/API.Benchmark/Program.cs +++ b/Kavita.Benchmark/Program.cs @@ -1,6 +1,6 @@ using BenchmarkDotNet.Running; -namespace API.Benchmark; +namespace Kavita.Benchmark; /// /// To build this, cd into API.Benchmark directory and run diff --git a/API.Benchmark/TestBenchmark.cs b/Kavita.Benchmark/TestBenchmark.cs similarity index 89% rename from API.Benchmark/TestBenchmark.cs rename to Kavita.Benchmark/TestBenchmark.cs index 511d250aa..2365683a0 100644 --- a/API.Benchmark/TestBenchmark.cs +++ b/Kavita.Benchmark/TestBenchmark.cs @@ -1,12 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using API.DTOs; -using API.Extensions; -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Order; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs; +using Kavita.Services.Extensions; -namespace API.Benchmark; +namespace Kavita.Benchmark; /// /// This is used as a scratchpad for testing diff --git a/API.Tests/Extensions/EnumerableExtensionsTests.cs b/Kavita.Common.Tests/Extensions/EnumerableExtensionsTests.cs similarity index 84% rename from API.Tests/Extensions/EnumerableExtensionsTests.cs rename to Kavita.Common.Tests/Extensions/EnumerableExtensionsTests.cs index bdd3433ae..deb048093 100644 --- a/API.Tests/Extensions/EnumerableExtensionsTests.cs +++ b/Kavita.Common.Tests/Extensions/EnumerableExtensionsTests.cs @@ -1,11 +1,6 @@ -using System.Collections.Generic; -using System.Linq; -using API.Data.Misc; -using API.Entities.Enums; -using API.Extensions; -using Xunit; +using Kavita.Common.Extensions; -namespace API.Tests.Extensions; +namespace Kavita.Common.Tests.Extensions; public class EnumerableExtensionsTests { @@ -135,33 +130,4 @@ public class EnumerableExtensionsTests i++; } } - - [Theory] - [InlineData(true, 2)] - [InlineData(false, 1)] - public void RestrictAgainstAgeRestriction_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount) - { - var items = new List() - { - new RecentlyAddedSeries() - { - AgeRating = AgeRating.Teen, - }, - new RecentlyAddedSeries() - { - AgeRating = AgeRating.Unknown, - }, - new RecentlyAddedSeries() - { - AgeRating = AgeRating.X18Plus, - }, - }; - - var filtered = items.RestrictAgainstAgeRestriction(new AgeRestriction() - { - AgeRating = AgeRating.Teen, - IncludeUnknowns = includeUnknowns - }); - Assert.Equal(expectedCount, filtered.Count()); - } } diff --git a/API.Tests/Extensions/PathExtensionsTests.cs b/Kavita.Common.Tests/Extensions/PathExtensionsTests.cs similarity index 81% rename from API.Tests/Extensions/PathExtensionsTests.cs rename to Kavita.Common.Tests/Extensions/PathExtensionsTests.cs index bdc752a92..fff2ed966 100644 --- a/API.Tests/Extensions/PathExtensionsTests.cs +++ b/Kavita.Common.Tests/Extensions/PathExtensionsTests.cs @@ -1,8 +1,6 @@ -using System.IO; -using Xunit; -using API.Extensions; +using Kavita.Common.Extensions; -namespace API.Tests.Extensions; +namespace Kavita.Common.Tests.Extensions; public class PathExtensionsTests { diff --git a/API.Tests/Extensions/VersionExtensionTests.cs b/Kavita.Common.Tests/Extensions/VersionExtensionTests.cs similarity index 95% rename from API.Tests/Extensions/VersionExtensionTests.cs rename to Kavita.Common.Tests/Extensions/VersionExtensionTests.cs index aee295370..ac6dee29e 100644 --- a/API.Tests/Extensions/VersionExtensionTests.cs +++ b/Kavita.Common.Tests/Extensions/VersionExtensionTests.cs @@ -1,8 +1,6 @@ -using System; -using API.Extensions; -using Xunit; +using Kavita.Common.Extensions; -namespace API.Tests.Extensions; +namespace Kavita.Common.Tests.Extensions; public class VersionHelperTests { diff --git a/API.Tests/Converters/CronConverterTests.cs b/Kavita.Common.Tests/Helpers/CronConverterTests.cs similarity index 83% rename from API.Tests/Converters/CronConverterTests.cs rename to Kavita.Common.Tests/Helpers/CronConverterTests.cs index 5568c89d0..86eeb3e62 100644 --- a/API.Tests/Converters/CronConverterTests.cs +++ b/Kavita.Common.Tests/Helpers/CronConverterTests.cs @@ -1,8 +1,7 @@ -using API.Helpers.Converters; -using Xunit; +using Kavita.Common.Helpers; + +namespace Kavita.Common.Tests.Helpers; -namespace API.Tests.Converters; -#nullable enable public class CronConverterTests { [Theory] diff --git a/Kavita.Common.Tests/Helpers/HtmlHelperTests.cs b/Kavita.Common.Tests/Helpers/HtmlHelperTests.cs new file mode 100644 index 000000000..b609a2823 --- /dev/null +++ b/Kavita.Common.Tests/Helpers/HtmlHelperTests.cs @@ -0,0 +1,134 @@ +using Kavita.Common.Helpers; + +namespace Kavita.Common.Tests.Helpers; + +public class HtmlHelperTests +{ + #region GetCharacters Tests + + [Fact] + public void GetCharacters_WithNullBody_ReturnsNull() + { + + string body = null; + + // Act + var result = HtmlHelper.GetCharacters(body); + + // Assert + Assert.Null(result); + } + + [Fact] + public void GetCharacters_WithEmptyBody_ReturnsEmptyString() + { + + var body = string.Empty; + + // Act + var result = HtmlHelper.GetCharacters(body); + + // Assert + Assert.Equal(string.Empty, result); + } + + [Fact] + public void GetCharacters_WithNoTextNodes_ReturnsEmptyString() + { + + const string body = "
"; + + // Act + var result = HtmlHelper.GetCharacters(body); + + // Assert + Assert.Equal(string.Empty, result); + } + + [Fact] + public void GetCharacters_WithLessCharactersThanLimit_ReturnsFullText() + { + + var body = "

This is a short review.

"; + + // Act + var result = HtmlHelper.GetCharacters(body); + + // Assert + Assert.Equal("This is a short review.…", result); + } + + [Fact] + public void GetCharacters_WithMoreCharactersThanLimit_TruncatesText() + { + + var body = "

" + new string('a', 200) + "

"; + + // Act + var result = HtmlHelper.GetCharacters(body); + + // Assert + Assert.Equal(new string('a', 175) + "…", result); + Assert.Equal(176, result.Length); // 175 characters + ellipsis + } + + [Fact] + public void GetCharacters_IgnoresScriptTags() + { + + const string body = "

Visible text

"; + + // Act + var result = HtmlHelper.GetCharacters(body); + + // Assert + Assert.Equal("Visible text…", result); + Assert.DoesNotContain("hidden", result); + } + + [Fact] + public void GetCharacters_RemovesMarkdownSymbols() + { + + const string body = "

This is **bold** and _italic_ text with [link](url).

"; + + // Act + var result = HtmlHelper.GetCharacters(body); + + // Assert + Assert.Equal("This is bold and italic text with link.…", result); + } + + [Fact] + public void GetCharacters_HandlesComplexMarkdownAndHtml() + { + + const string body = """ + +
+

# Header

+

This is ~~strikethrough~~ and __underlined__ text

+

~~~code block~~~

+

+++highlighted+++

+

img123(image.jpg)

+
+ """; + + // Act + var result = HtmlHelper.GetCharacters(body); + + // Assert + Assert.DoesNotContain("~~", result); + Assert.DoesNotContain("__", result); + Assert.DoesNotContain("~~~", result); + Assert.DoesNotContain("+++", result); + Assert.DoesNotContain("img123(", result); + Assert.Contains("Header", result); + Assert.Contains("strikethrough", result); + Assert.Contains("underlined", result); + Assert.Contains("code block", result); + Assert.Contains("highlighted", result); + } + + #endregion +} diff --git a/API.Tests/Helpers/RandfHelper.cs b/Kavita.Common.Tests/Helpers/RandfHelper.cs similarity index 96% rename from API.Tests/Helpers/RandfHelper.cs rename to Kavita.Common.Tests/Helpers/RandfHelper.cs index 01a6a2df5..9ca02b912 100644 --- a/API.Tests/Helpers/RandfHelper.cs +++ b/Kavita.Common.Tests/Helpers/RandfHelper.cs @@ -1,11 +1,10 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; -namespace API.Tests.Helpers; +namespace Kavita.Common.Tests.Helpers; +/// +/// This is not a test class, but a helper to help you write test that require random interactions +/// public static class RandfHelper { private static readonly Random Random = new (); diff --git a/API.Tests/Helpers/RateLimiterTests.cs b/Kavita.Common.Tests/Helpers/RateLimiterTests.cs similarity index 93% rename from API.Tests/Helpers/RateLimiterTests.cs rename to Kavita.Common.Tests/Helpers/RateLimiterTests.cs index bf7827106..f7c58e130 100644 --- a/API.Tests/Helpers/RateLimiterTests.cs +++ b/Kavita.Common.Tests/Helpers/RateLimiterTests.cs @@ -1,9 +1,6 @@ -using System; -using System.Threading.Tasks; -using API.Helpers; -using Xunit; +using Kavita.Common.Helpers; -namespace API.Tests.Helpers; +namespace Kavita.Common.Tests.Helpers; public class RateLimiterTests { diff --git a/API.Tests/Helpers/StringHelperTests.cs b/Kavita.Common.Tests/Helpers/StringHelperTests.cs similarity index 96% rename from API.Tests/Helpers/StringHelperTests.cs rename to Kavita.Common.Tests/Helpers/StringHelperTests.cs index 8f845c9b0..9a3d11d5c 100644 --- a/API.Tests/Helpers/StringHelperTests.cs +++ b/Kavita.Common.Tests/Helpers/StringHelperTests.cs @@ -1,7 +1,6 @@ -using API.Helpers; -using Xunit; +using Kavita.Common.Helpers; -namespace API.Tests.Helpers; +namespace Kavita.Common.Tests.Helpers; public class StringHelperTests { diff --git a/Kavita.Common.Tests/Kavita.Common.Tests.csproj b/Kavita.Common.Tests/Kavita.Common.Tests.csproj new file mode 100644 index 000000000..6ac641f8e --- /dev/null +++ b/Kavita.Common.Tests/Kavita.Common.Tests.csproj @@ -0,0 +1,31 @@ + + + + net10.0 + enable + enable + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + \ No newline at end of file diff --git a/API/Constants/Headers.cs b/Kavita.Common/Constants/Headers.cs similarity index 93% rename from API/Constants/Headers.cs rename to Kavita.Common/Constants/Headers.cs index 7f44b30ac..df8d590b8 100644 --- a/API/Constants/Headers.cs +++ b/Kavita.Common/Constants/Headers.cs @@ -1,4 +1,4 @@ -namespace API.Constants; +namespace Kavita.Common.Constants; public static class Headers { diff --git a/API/Extensions/ClaimsPrincipalExtensions.cs b/Kavita.Common/Extensions/ClaimsPrincipalExtensions.cs similarity index 89% rename from API/Extensions/ClaimsPrincipalExtensions.cs rename to Kavita.Common/Extensions/ClaimsPrincipalExtensions.cs index ea1cb5355..c83a694d0 100644 --- a/API/Extensions/ClaimsPrincipalExtensions.cs +++ b/Kavita.Common/Extensions/ClaimsPrincipalExtensions.cs @@ -1,11 +1,9 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Security.Claims; -using Kavita.Common; -using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames; -namespace API.Extensions; -#nullable enable +namespace Kavita.Common.Extensions; public static class ClaimsPrincipalExtensions { diff --git a/API/Extensions/DateTimeExtensions.cs b/Kavita.Common/Extensions/DateTimeExtensions.cs similarity index 94% rename from API/Extensions/DateTimeExtensions.cs rename to Kavita.Common/Extensions/DateTimeExtensions.cs index 89c930afc..43a945396 100644 --- a/API/Extensions/DateTimeExtensions.cs +++ b/Kavita.Common/Extensions/DateTimeExtensions.cs @@ -1,7 +1,6 @@ -using System; +using System; -namespace API.Extensions; -#nullable enable +namespace Kavita.Common.Extensions; public static class DateTimeExtensions { diff --git a/API/Extensions/DoubleExtensions.cs b/Kavita.Common/Extensions/DoubleExtensions.cs similarity index 92% rename from API/Extensions/DoubleExtensions.cs rename to Kavita.Common/Extensions/DoubleExtensions.cs index 3deb37ffb..d66e56943 100644 --- a/API/Extensions/DoubleExtensions.cs +++ b/Kavita.Common/Extensions/DoubleExtensions.cs @@ -1,6 +1,6 @@ -using System; +using System; -namespace API.Extensions; +namespace Kavita.Common.Extensions; public static class DoubleExtensions { diff --git a/Kavita.Common/Extensions/EnumExtensions.cs b/Kavita.Common/Extensions/EnumExtensions.cs index e672d8050..83f8c2fe7 100644 --- a/Kavita.Common/Extensions/EnumExtensions.cs +++ b/Kavita.Common/Extensions/EnumExtensions.cs @@ -1,4 +1,6 @@ -using System.ComponentModel; +using System; +using System.ComponentModel; +using System.Reflection; namespace Kavita.Common.Extensions; @@ -17,4 +19,38 @@ public static class EnumExtensions return attributes is {Length: > 0} ? attributes[0].Description : value.ToString(); } + + /// + /// Extension on Enum.TryParse which also tried matching on the description attribute + /// + /// if a match was found + /// First tries Enum.TryParse then fall back to the more expensive operation + public static bool TryParse(string? value, out TEnum result) where TEnum : struct, Enum + { + result = default; + + if (string.IsNullOrEmpty(value)) + { + return false; + } + + if (Enum.TryParse(value, out result)) + { + return true; + } + + foreach (var field in typeof(TEnum).GetFields(BindingFlags.Public | BindingFlags.Static)) + { + var description = field.GetCustomAttribute()?.Description; + + if (!string.IsNullOrEmpty(description) && + string.Equals(description, value, StringComparison.OrdinalIgnoreCase)) + { + result = (TEnum)field.GetValue(null)!; + return true; + } + } + + return false; + } } diff --git a/Kavita.Common/Extensions/EnumerableExtensions.cs b/Kavita.Common/Extensions/EnumerableExtensions.cs new file mode 100644 index 000000000..b8e0ba40e --- /dev/null +++ b/Kavita.Common/Extensions/EnumerableExtensions.cs @@ -0,0 +1,68 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Kavita.Common.Extensions; + +public static class EnumerableExtensions +{ + private static readonly Regex Regex = new Regex(@"\d+", RegexOptions.Compiled, TimeSpan.FromMilliseconds(500)); + + /// + /// A natural sort implementation + /// + /// IEnumerable to process + /// Function that produces a string. Does not support null values + /// Defaults to CurrentCulture + /// + /// Sorted Enumerable + public static IEnumerable OrderByNatural(this IEnumerable items, Func selector, StringComparer? stringComparer = null) + { + var list = items.ToList(); + var maxDigits = list + .SelectMany(i => Regex.Matches(selector(i)) + .Select(digitChunk => (int?)digitChunk.Value.Length)) + .Max() ?? 0; + + return list.OrderBy(i => Regex.Replace(selector(i), match => match.Value.PadLeft(maxDigits, '0')), stringComparer ?? StringComparer.CurrentCulture); + } + + /// + /// + extension(IList source) + { + /// + /// Safety net around Max, returning the default value if the source contains no elements + /// + /// + /// + /// + /// + public TResult? MaxOrDefault(Func selector, + TResult? defaultValue) + { + return source.Count == 0 ? defaultValue : source.Max(selector); + } + + /// + /// Safety wrapper around Min, returning the default value if the source has no elements + /// + /// + /// + /// + /// + public TResult? MinOrDefault(Func selector, + TResult? defaultValue) + { + return source.Count == 0 ? defaultValue : source.Min(selector); + } + } + + public static IEnumerable WhereNotNull(this IEnumerable source) + where TSource : class + { + return source.Where(item => item != null)!; + } +} diff --git a/API/Extensions/FloatExtensions.cs b/Kavita.Common/Extensions/FloatExtensions.cs similarity index 91% rename from API/Extensions/FloatExtensions.cs rename to Kavita.Common/Extensions/FloatExtensions.cs index 6fa553239..d9facfe20 100644 --- a/API/Extensions/FloatExtensions.cs +++ b/Kavita.Common/Extensions/FloatExtensions.cs @@ -1,6 +1,6 @@ -using System; +using System; -namespace API.Extensions; +namespace Kavita.Common.Extensions; public static class FloatExtensions { diff --git a/API/Extensions/FlurlExtensions.cs b/Kavita.Common/Extensions/FlurlExtensions.cs similarity index 93% rename from API/Extensions/FlurlExtensions.cs rename to Kavita.Common/Extensions/FlurlExtensions.cs index a26e53914..e385cdc66 100644 --- a/API/Extensions/FlurlExtensions.cs +++ b/Kavita.Common/Extensions/FlurlExtensions.cs @@ -1,15 +1,13 @@ using System; -using API.Constants; using System.Linq; using System.Threading.Tasks; using Flurl.Http; -using Kavita.Common; +using Kavita.Common.Constants; using Kavita.Common.EnvironmentInfo; -using Microsoft.Net.Http.Headers; using Microsoft.AspNetCore.StaticFiles; +using Microsoft.Net.Http.Headers; -namespace API.Extensions; -#nullable enable +namespace Kavita.Common.Extensions; public static class FlurlExtensions { @@ -34,8 +32,7 @@ public static class FlurlExtensions return null; } - // TODO: Move to new Headers class after merge with progress branch - var contentTypeHeader = headResponse.Headers.FirstOrDefault("Content-Type"); + var contentTypeHeader = headResponse.Headers.FirstOrDefault(HeaderNames.ContentType); if (string.IsNullOrEmpty(contentTypeHeader)) { return null; diff --git a/API/Extensions/ImageExtensions.cs b/Kavita.Common/Extensions/ImageExtensions.cs similarity index 99% rename from API/Extensions/ImageExtensions.cs rename to Kavita.Common/Extensions/ImageExtensions.cs index 5779b18ec..58bfea11d 100644 --- a/API/Extensions/ImageExtensions.cs +++ b/Kavita.Common/Extensions/ImageExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -7,7 +7,7 @@ using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using Image = SixLabors.ImageSharp.Image; -namespace API.Extensions; +namespace Kavita.Common.Extensions; public static class ImageExtensions { diff --git a/Kavita.Common/Extensions/PathExtensions.cs b/Kavita.Common/Extensions/PathExtensions.cs index 904589630..b04290241 100644 --- a/Kavita.Common/Extensions/PathExtensions.cs +++ b/Kavita.Common/Extensions/PathExtensions.cs @@ -4,8 +4,11 @@ namespace Kavita.Common.Extensions; public static class PathExtensions { - public static string GetParentDirectory(string filePath) + public static string GetFullPathWithoutExtension(this string filepath) { - return Path.GetDirectoryName(filePath); + if (string.IsNullOrEmpty(filepath)) return filepath; + var extension = Path.GetExtension(filepath); + if (string.IsNullOrEmpty(extension)) return filepath; + return Path.GetFullPath(filepath.Replace(extension, string.Empty)); } } diff --git a/Kavita.Common/Extensions/StringExtensions.cs b/Kavita.Common/Extensions/StringExtensions.cs new file mode 100644 index 000000000..333b1c169 --- /dev/null +++ b/Kavita.Common/Extensions/StringExtensions.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Kavita.Common.Extensions; +#nullable enable + +public static partial class StringExtensions +{ + private static readonly Regex SentenceCaseRegex = new(@"(^[a-z])|\.\s+(.)", + RegexOptions.ExplicitCapture | RegexOptions.Compiled, + TimeSpan.FromMilliseconds(500)); + + /// + /// Normalize everything within Kavita. Some characters don't fall under Unicode, like full-width characters and need to be + /// added on a case-by-case basis. + /// + private static readonly Regex NormalizeRegex = new(@"[^\p{L}0-9\+!*!+]", + RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant, TimeSpan.FromMilliseconds(500)); + + extension(string input) + { + public string Sanitize() + { + if (string.IsNullOrEmpty(input)) + return string.Empty; + + // Remove all newline and control characters + var sanitized = input + .Replace(Environment.NewLine, string.Empty) + .Replace("\n", string.Empty) + .Replace("\r", string.Empty); + + // Optionally remove other potentially unwanted characters + sanitized = Regex.Replace(sanitized, @"[^\u0020-\u007E]", string.Empty); // Removes non-printable ASCII + + return sanitized.Trim(); // Trim any leading/trailing whitespace + } + + public string SentenceCase() + { + return SentenceCaseRegex.Replace(input.ToLower(), s => s.Value.ToUpper()); + } + } + + /// + extension(string? value) + { + /// + /// Apply normalization on the String + /// + /// + public string ToNormalized() + { + return string.IsNullOrEmpty(value) ? string.Empty : NormalizeRegex.Replace(value, string.Empty).Trim().ToLower(); + } + + /// + /// Normalizes the slashes in a path to be + /// + /// /manga/1\1 -> /manga/1/1 + /// + public string NormalizePath() + { + return string.IsNullOrEmpty(value) ? string.Empty : value.Replace('\\', Path.AltDirectorySeparatorChar) + .Replace(@"//", Path.AltDirectorySeparatorChar + string.Empty); + } + + public float AsFloat(float defaultValue = 0.0f) + { + return string.IsNullOrEmpty(value) ? defaultValue : float.Parse(value, CultureInfo.InvariantCulture); + } + + public double AsDouble(double defaultValue = 0.0f) + { + return string.IsNullOrEmpty(value) ? defaultValue : double.Parse(value, CultureInfo.InvariantCulture); + } + + public string TrimPrefix(string prefix) + { + if (string.IsNullOrEmpty(value)) return string.Empty; + + if (!value.StartsWith(prefix)) return value; + + return value.Substring(prefix.Length); + } + + /// + /// Censor the input string by removing all but the first and last char. + /// + /// + /// If the input is an email (contains @), the domain will remain untouched + public string Censor() + { + if (string.IsNullOrWhiteSpace(value)) return value ?? string.Empty; + + var atIdx = value.IndexOf('@'); + if (atIdx == -1) + { + return $"{value[0]}{new string('*', value.Length - 1)}"; + } + + return value[0] + new string('*', atIdx - 1) + value[atIdx..]; + } + + /// + /// Repeat returns a string that is equal to the original string repeat n times + /// + /// Amount of times to repeat + /// + public string Repeat(int n) + { + return string.IsNullOrEmpty(value) ? string.Empty : string.Concat(Enumerable.Repeat(value, n)); + } + } + + extension(string value) + { + /// + /// Splits the string by the given separator. While cleaning out entries and removing duplicates + /// + /// + /// + public IList SplitBy(char separator) + { + if (string.IsNullOrEmpty(value)) + { + return ImmutableList.Empty; + } + + return value.Split(separator, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) + .DistinctBy(s => s.ToNormalized()) + .ToList(); + } + + public IList ParseIntArray() + { + if (string.IsNullOrWhiteSpace(value)) + { + return []; + } + + return value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(int.Parse) + .ToList(); + } + + /// + /// Parses a human-readable file size string (e.g. "1.43 GB") into bytes. + /// + /// Byte count as long + /// The input string like "1.43 GB", "4.2 KB", "512 B" + public long ParseHumanReadableBytes() + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException("Input cannot be null or empty.", nameof(value)); + } + + + var match = HumanReadableBytesRegex().Match(value); + if (!match.Success) + { + throw new FormatException($"Invalid format: '{value}'"); + } + + + var value1 = double.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); + var unit = match.Groups[2].Value.ToUpperInvariant(); + + var multiplier = unit switch + { + "B" => 1L, + "KB" => 1L << 10, + "MB" => 1L << 20, + "GB" => 1L << 30, + "TB" => 1L << 40, + "PB" => 1L << 50, + "EB" => 1L << 60, + _ => throw new FormatException($"Unknown unit: '{unit}'") + }; + + return (long)(value1 * multiplier); + } + } + + [GeneratedRegex(@"^\s*(\d+(?:\.\d+)?)\s*([KMGTPE]?B)\s*$", RegexOptions.IgnoreCase)] + private static partial Regex HumanReadableBytesRegex(); +} diff --git a/API/Extensions/VersionExtensions.cs b/Kavita.Common/Extensions/VersionExtensions.cs similarity index 88% rename from API/Extensions/VersionExtensions.cs rename to Kavita.Common/Extensions/VersionExtensions.cs index 1877b48b1..d6f7b7d76 100644 --- a/API/Extensions/VersionExtensions.cs +++ b/Kavita.Common/Extensions/VersionExtensions.cs @@ -1,6 +1,6 @@ -using System; +using System; -namespace API.Extensions; +namespace Kavita.Common.Extensions; public static class VersionExtensions { diff --git a/API/Helpers/AuthKeyHelper.cs b/Kavita.Common/Helpers/AuthKeyHelper.cs similarity index 95% rename from API/Helpers/AuthKeyHelper.cs rename to Kavita.Common/Helpers/AuthKeyHelper.cs index 94a6e04d2..211a6932e 100644 --- a/API/Helpers/AuthKeyHelper.cs +++ b/Kavita.Common/Helpers/AuthKeyHelper.cs @@ -1,7 +1,7 @@ using System; using System.Security.Cryptography; -namespace API.Helpers; +namespace Kavita.Common.Helpers; public static class AuthKeyHelper { diff --git a/API/Helpers/Converters/CronConverter.cs b/Kavita.Common/Helpers/CronConverter.cs similarity index 77% rename from API/Helpers/Converters/CronConverter.cs rename to Kavita.Common/Helpers/CronConverter.cs index f1f0ebc1b..ff8f94cb8 100644 --- a/API/Helpers/Converters/CronConverter.cs +++ b/Kavita.Common/Helpers/CronConverter.cs @@ -1,17 +1,16 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Hangfire; -namespace API.Helpers.Converters; -#nullable enable +namespace Kavita.Common.Helpers; public static class CronConverter { - public static readonly IEnumerable Options = new [] - { + public static readonly IEnumerable Options = + [ "disabled", "daily", - "weekly", - }; + "weekly" + ]; /// /// Converts to Cron Notation /// diff --git a/API/Helpers/DayOfWeekHelper.cs b/Kavita.Common/Helpers/DayOfWeekHelper.cs similarity index 89% rename from API/Helpers/DayOfWeekHelper.cs rename to Kavita.Common/Helpers/DayOfWeekHelper.cs index 10cdb4170..c12fc02c3 100644 --- a/API/Helpers/DayOfWeekHelper.cs +++ b/Kavita.Common/Helpers/DayOfWeekHelper.cs @@ -1,6 +1,6 @@ -using System; +using System; -namespace API.Helpers; +namespace Kavita.Common.Helpers; public static class DayOfWeekHelper { diff --git a/API/Helpers/ReviewHelper.cs b/Kavita.Common/Helpers/HtmlHelper.cs similarity index 62% rename from API/Helpers/ReviewHelper.cs rename to Kavita.Common/Helpers/HtmlHelper.cs index a5b9f35d3..3c0f00ab3 100644 --- a/API/Helpers/ReviewHelper.cs +++ b/Kavita.Common/Helpers/HtmlHelper.cs @@ -1,51 +1,16 @@ -using System; -using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; -using API.DTOs.SeriesDetail; using HtmlAgilityPack; -namespace API.Helpers; +namespace Kavita.Common.Helpers; -public static class ReviewHelper +#nullable enable + +public static class HtmlHelper { private const int BodyTextLimit = 175; - public static IEnumerable 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.OrderByDescending(r => r.Score); - } - - public static string GetCharacters(string body) + public static string? GetCharacters(string? body) { if (string.IsNullOrEmpty(body)) return body; @@ -54,6 +19,7 @@ public static class ReviewHelper var textNodes = doc.DocumentNode.SelectNodes("//text()[not(parent::script)]"); if (textNodes == null) return string.Empty; + var plainText = string.Join(" ", textNodes .Select(node => node.InnerText) .Where(s => !s.Equals("\n"))); @@ -84,5 +50,4 @@ public static class ReviewHelper return plainText + "…"; } - } diff --git a/API/Helpers/JwtHelper.cs b/Kavita.Common/Helpers/JwtHelper.cs similarity index 96% rename from API/Helpers/JwtHelper.cs rename to Kavita.Common/Helpers/JwtHelper.cs index c4dc99125..4fe2cf4d0 100644 --- a/API/Helpers/JwtHelper.cs +++ b/Kavita.Common/Helpers/JwtHelper.cs @@ -1,7 +1,7 @@ using System; using System.IdentityModel.Tokens.Jwt; -namespace API.Helpers; +namespace Kavita.Common.Helpers; public static class JwtHelper { diff --git a/API/Helpers/NumberHelper.cs b/Kavita.Common/Helpers/NumberHelper.cs similarity index 84% rename from API/Helpers/NumberHelper.cs rename to Kavita.Common/Helpers/NumberHelper.cs index 906e405cc..06c41ab2b 100644 --- a/API/Helpers/NumberHelper.cs +++ b/Kavita.Common/Helpers/NumberHelper.cs @@ -1,4 +1,4 @@ -namespace API.Helpers; +namespace Kavita.Common.Helpers; #nullable enable public static class NumberHelper diff --git a/Kavita.Common/Helpers/PagedList.cs b/Kavita.Common/Helpers/PagedList.cs new file mode 100644 index 000000000..d633dd126 --- /dev/null +++ b/Kavita.Common/Helpers/PagedList.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace Kavita.Common.Helpers; + +public class PagedList : List +{ + private PagedList(IEnumerable items, int count, int pageNumber, int pageSize) + { + CurrentPage = pageNumber; + TotalPages = (int) Math.Ceiling(count / (double) pageSize); + PageSize = pageSize; + TotalCount = count; + AddRange(items); + } + + public int CurrentPage { get; set; } + public int TotalPages { get; set; } + public int PageSize { get; set; } + public int TotalCount { get; set; } + + public static PagedList Create(IEnumerable items, int totalCount, int pageNumber, int pageSize) + { + return new PagedList(items, totalCount, pageNumber, pageSize); + } +} diff --git a/API/Helpers/PaginationHeader.cs b/Kavita.Common/Helpers/PaginationHeader.cs similarity index 91% rename from API/Helpers/PaginationHeader.cs rename to Kavita.Common/Helpers/PaginationHeader.cs index b11c5ecd4..bfa5b3e8d 100644 --- a/API/Helpers/PaginationHeader.cs +++ b/Kavita.Common/Helpers/PaginationHeader.cs @@ -1,5 +1,4 @@ -namespace API.Helpers; -#nullable enable +namespace Kavita.Common.Helpers; public class PaginationHeader { diff --git a/API/Helpers/RateLimiter.cs b/Kavita.Common/Helpers/RateLimiter.cs similarity index 98% rename from API/Helpers/RateLimiter.cs rename to Kavita.Common/Helpers/RateLimiter.cs index ffdcadc9c..735a96a94 100644 --- a/API/Helpers/RateLimiter.cs +++ b/Kavita.Common/Helpers/RateLimiter.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Threading; -namespace API.Helpers; +namespace Kavita.Common.Helpers; public class RateLimiter(int maxRequests, TimeSpan duration, bool refillBetween = true) { diff --git a/API/Helpers/StringHelper.cs b/Kavita.Common/Helpers/StringHelper.cs similarity index 98% rename from API/Helpers/StringHelper.cs rename to Kavita.Common/Helpers/StringHelper.cs index 0a20910c5..e34a019b7 100644 --- a/API/Helpers/StringHelper.cs +++ b/Kavita.Common/Helpers/StringHelper.cs @@ -1,6 +1,6 @@ using System.Text.RegularExpressions; -namespace API.Helpers; +namespace Kavita.Common.Helpers; #nullable enable public static partial class StringHelper diff --git a/API/Helpers/UserParams.cs b/Kavita.Common/Helpers/UserParams.cs similarity index 96% rename from API/Helpers/UserParams.cs rename to Kavita.Common/Helpers/UserParams.cs index 24faf6a8d..ff6a57feb 100644 --- a/API/Helpers/UserParams.cs +++ b/Kavita.Common/Helpers/UserParams.cs @@ -1,4 +1,4 @@ -namespace API.Helpers; +namespace Kavita.Common.Helpers; #nullable enable /// diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index 436c0f0b4..c91ef05a5 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -9,15 +9,23 @@ + - - - + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive + \ No newline at end of file diff --git a/API.Tests/AbstractDbTest.cs b/Kavita.Database.Tests/AbstractDbTest.cs similarity index 81% rename from API.Tests/AbstractDbTest.cs rename to Kavita.Database.Tests/AbstractDbTest.cs index 79fa075c8..7962c27a6 100644 --- a/API.Tests/AbstractDbTest.cs +++ b/Kavita.Database.Tests/AbstractDbTest.cs @@ -1,23 +1,20 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.Data.AutoMapper; -using API.Entities; -using API.Entities.Enums; -using API.Helpers; -using API.Helpers.Builders; -using API.Services; using AutoMapper; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.Models.AutoMapper; +using Kavita.Models.Builders; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; using NSubstitute; using Xunit.Abstractions; -namespace API.Tests; -#nullable enable +namespace Kavita.Database.Tests; public abstract class AbstractDbTest(ITestOutputHelper testOutputHelper): AbstractFsTest, IAsyncDisposable { @@ -27,6 +24,9 @@ public abstract class AbstractDbTest(ITestOutputHelper testOutputHelper): Abstra protected async Task<(IUnitOfWork, DataContext, IMapper)> CreateDatabase() { + + GlobalConfiguration.Configuration.UseInMemoryStorage(); + // Dispose any previous connection if CreateDatabase is called multiple times if (_connection != null) { @@ -36,9 +36,8 @@ public abstract class AbstractDbTest(ITestOutputHelper testOutputHelper): Abstra _connection = new SqliteConnection("Filename=:memory:"); await _connection.OpenAsync(); - var contextOptions = new DbContextOptionsBuilder() - .UseSqlite(_connection) - .EnableSensitiveDataLogging() + var contextOptions = ((DbContextOptionsBuilder)new DbContextOptionsBuilder() + .UseSqlite(_connection)).EnableSensitiveDataLogging() .Options; _context = new DataContext(contextOptions); @@ -54,20 +53,21 @@ public abstract class AbstractDbTest(ITestOutputHelper testOutputHelper): Abstra }); var mapper = config.CreateMapper(); - GlobalConfiguration.Configuration.UseInMemoryStorage(); - var unitOfWork = new UnitOfWork(_context, mapper, null); + var unitOfWork = new UnitOfWork(_context, mapper, null!); _context.ChangeTracker.Clear(); return (unitOfWork, _context, mapper); } - private async Task SeedDb(DataContext context) + private async Task SeedDb(DataContext context) { try { - var filesystem = CreateFileSystem(); - await Seed.SeedSettings(context, new DirectoryService(Substitute.For>(), filesystem)); + var directoryService = Substitute.For(); + directoryService.BackupDirectory.Returns(BackupDirectory); + + await Seed.SeedSettings(context, directoryService); var setting = await context.ServerSetting.Where(s => s.Key == ServerSettingKey.CacheDirectory).SingleAsync(); setting.Value = CacheDirectory; @@ -92,19 +92,18 @@ public abstract class AbstractDbTest(ITestOutputHelper testOutputHelper): Abstra await context.SaveChangesAsync(); await Seed.SeedMetadataSettings(context); - - return true; } catch (Exception ex) { testOutputHelper.WriteLine($"[SeedDb] Error: {ex.Message} \n{ex.StackTrace}"); - return false; + throw; } } /// /// Add a role to an existing User. Commits. /// + /// /// /// protected static async Task AddUserWithRole(DataContext context, int userId, string roleName) diff --git a/API.Tests/AbstractFsTest.cs b/Kavita.Database.Tests/AbstractFsTest.cs similarity index 90% rename from API.Tests/AbstractFsTest.cs rename to Kavita.Database.Tests/AbstractFsTest.cs index 0c6a0e262..4afc27e2d 100644 --- a/API.Tests/AbstractFsTest.cs +++ b/Kavita.Database.Tests/AbstractFsTest.cs @@ -1,14 +1,13 @@ using System.IO; using System.IO.Abstractions.TestingHelpers; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Common.Extensions; -namespace API.Tests; -#nullable enable +namespace Kavita.Database.Tests; public abstract class AbstractFsTest { - protected static readonly string Root = Parser.NormalizePath(Path.GetPathRoot(Directory.GetCurrentDirectory())); + protected static readonly string Root = Path.GetPathRoot(Directory.GetCurrentDirectory()).NormalizePath(); protected static readonly string ConfigDirectory = Root + "kavita/config/"; protected static readonly string CacheDirectory = ConfigDirectory + "cache/"; protected static readonly string CacheLongDirectory = ConfigDirectory + "cache-long/"; diff --git a/API.Tests/Extensions/QueryableExtensionsTests.cs b/Kavita.Database.Tests/Extensions/QueryableExtensionsTests.cs similarity index 98% rename from API.Tests/Extensions/QueryableExtensionsTests.cs rename to Kavita.Database.Tests/Extensions/QueryableExtensionsTests.cs index 15e02430a..9c112585c 100644 --- a/API.Tests/Extensions/QueryableExtensionsTests.cs +++ b/Kavita.Database.Tests/Extensions/QueryableExtensionsTests.cs @@ -1,17 +1,16 @@ using System.Collections.Generic; using System.Linq; -using API.Data.Misc; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Enums.UserPreferences; -using API.Entities.Metadata; -using API.Entities.Person; -using API.Entities.User; -using API.Extensions.QueryExtensions; -using API.Helpers.Builders; +using Kavita.Database.Extensions; +using Kavita.Models.Builders; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.Person; +using Kavita.Models.Entities.User; using Xunit; -namespace API.Tests.Extensions; +namespace Kavita.Database.Tests.Extensions; public class QueryableExtensionsTests { diff --git a/Kavita.Database.Tests/Kavita.Database.Tests.csproj b/Kavita.Database.Tests/Kavita.Database.Tests.csproj new file mode 100644 index 000000000..f15cc7299 --- /dev/null +++ b/Kavita.Database.Tests/Kavita.Database.Tests.csproj @@ -0,0 +1,32 @@ + + + + net10.0 + disable + enable + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + \ No newline at end of file diff --git a/Kavita.Database.Tests/Repositories/ExternalSeriesMetadataRepositoryTests.cs b/Kavita.Database.Tests/Repositories/ExternalSeriesMetadataRepositoryTests.cs new file mode 100644 index 000000000..f7b6e58ba --- /dev/null +++ b/Kavita.Database.Tests/Repositories/ExternalSeriesMetadataRepositoryTests.cs @@ -0,0 +1,284 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Kavita.Models.Builders; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Xunit; +using Xunit.Abstractions; + +namespace Kavita.Database.Tests.Repositories; + +public class ExternalSeriesMetadataRepositoryTests(ITestOutputHelper outputHelper) : AbstractDbTest(outputHelper) +{ + [Fact] + public async Task NeedsDataRefresh_WhenValidUntilIsInThePast_ReturnsTrue() + { + var (unitOfWork, context, _) = await CreateDatabase(); + + var lib = new LibraryBuilder("lib0") + .WithSeries(new SeriesBuilder("series0").Build()) + .Build(); + context.Library.Add(lib); + await context.SaveChangesAsync(); + + var series = context.Series.First(s => s.Name == "series0"); + + var metadata = new ExternalSeriesMetadata + { + SeriesId = series.Id, + ValidUntilUtc = DateTime.UtcNow.AddDays(-1) // expired yesterday + }; + context.ExternalSeriesMetadata.Add(metadata); + await context.SaveChangesAsync(); + + var result = await unitOfWork.ExternalSeriesMetadataRepository.NeedsDataRefresh(series.Id); + + Assert.True(result); + } + + [Fact] + public async Task NeedsDataRefresh_WhenValidUntilIsInTheFuture_ReturnsFalse() + { + var (unitOfWork, context, _) = await CreateDatabase(); + + var lib = new LibraryBuilder("lib0") + .WithSeries(new SeriesBuilder("series0").Build()) + .Build(); + context.Library.Add(lib); + await context.SaveChangesAsync(); + + var series = context.Series.First(s => s.Name == "series0"); + + var metadata = new ExternalSeriesMetadata + { + SeriesId = series.Id, + ValidUntilUtc = DateTime.UtcNow.AddDays(7) // valid for another week + }; + context.ExternalSeriesMetadata.Add(metadata); + await context.SaveChangesAsync(); + + var result = await unitOfWork.ExternalSeriesMetadataRepository.NeedsDataRefresh(series.Id); + + Assert.False(result); + } + + [Fact] + public async Task NeedsDataRefresh_OnlyChecksRequestedSeries() + { + var (unitOfWork, context, _) = await CreateDatabase(); + + var lib = new LibraryBuilder("lib0") + .WithSeries(new SeriesBuilder("series0").Build()) + .WithSeries(new SeriesBuilder("series1").Build()) + .Build(); + context.Library.Add(lib); + await context.SaveChangesAsync(); + + var series0 = context.Series.First(s => s.Name == "series0"); + var series1 = context.Series.First(s => s.Name == "series1"); + + context.ExternalSeriesMetadata.AddRange( + new ExternalSeriesMetadata { SeriesId = series0.Id, ValidUntilUtc = DateTime.UtcNow.AddDays(-1) }, + new ExternalSeriesMetadata { SeriesId = series1.Id, ValidUntilUtc = DateTime.UtcNow.AddDays(7) } + ); + await context.SaveChangesAsync(); + + var staleSeries = await unitOfWork.ExternalSeriesMetadataRepository.NeedsDataRefresh(series0.Id); + var freshSeries = await unitOfWork.ExternalSeriesMetadataRepository.NeedsDataRefresh(series1.Id); + + Assert.True(staleSeries); + Assert.False(freshSeries); + } + + [Fact] + public async Task GetSeriesDetailPlusDto_WithRatings_ReturnsAllRatings() + { + var (unitOfWork, context, _) = await CreateDatabase(); + + var lib = new LibraryBuilder("lib0") + .WithSeries(new SeriesBuilder("series0").Build()) + .Build(); + context.Library.Add(lib); + await context.SaveChangesAsync(); + + var series = context.Series.First(s => s.Name == "series0"); + + var metadata = new ExternalSeriesMetadata + { + SeriesId = series.Id, + ValidUntilUtc = DateTime.UtcNow.AddDays(7), + ExternalRatings = new List + { + new() { Provider = ScrobbleProvider.AniList, AverageScore = 85, FavoriteCount = 1000 }, + new() { Provider = ScrobbleProvider.Mal, AverageScore = 90, FavoriteCount = 2000 } + } + }; + context.ExternalSeriesMetadata.Add(metadata); + await context.SaveChangesAsync(); + + var result = await unitOfWork.ExternalSeriesMetadataRepository.GetSeriesDetailPlusDto(series.Id); + + Assert.NotNull(result); + Assert.Equal(2, result.Ratings?.Count()); + } + + [Fact] + public async Task GetSeriesDetailPlusDto_WithReviews_ReturnsReviewsSortedByScoreDescending() + { + var (unitOfWork, context, _) = await CreateDatabase(); + + var lib = new LibraryBuilder("lib0") + .WithSeries(new SeriesBuilder("series0").Build()) + .Build(); + context.Library.Add(lib); + await context.SaveChangesAsync(); + + var series = context.Series.First(s => s.Name == "series0"); + + var metadata = new ExternalSeriesMetadata + { + SeriesId = series.Id, + ValidUntilUtc = DateTime.UtcNow.AddDays(7), + ExternalReviews = new List + { + new() { Score = 60, Body = "Decent", Username = "user1", Provider = ScrobbleProvider.AniList, BodyJustText = string.Empty}, + new() { Score = 95, Body = "Excellent", Username = "user2", Provider = ScrobbleProvider.AniList, BodyJustText = string.Empty }, + new() { Score = 80, Body = "Good", Username = "user3", Provider = ScrobbleProvider.AniList, BodyJustText = string.Empty } + } + }; + context.ExternalSeriesMetadata.Add(metadata); + await context.SaveChangesAsync(); + + var result = await unitOfWork.ExternalSeriesMetadataRepository.GetSeriesDetailPlusDto(series.Id); + + Assert.NotNull(result); + var reviews = result.Reviews.ToList(); + Assert.Equal(3, reviews.Count); + Assert.Equal(95, reviews[0].Score); + Assert.Equal(80, reviews[1].Score); + Assert.Equal(60, reviews[2].Score); + Assert.All(reviews, r => Assert.True(r.IsExternal)); + } + + [Fact] + public async Task GetSeriesDetailPlusDto_WithRecommendations_SplitsOwnedAndExternalCorrectly() + { + var (unitOfWork, context, _) = await CreateDatabase(); + + var lib = new LibraryBuilder("lib0") + .WithSeries(new SeriesBuilder("series0").Build()) + .WithSeries(new SeriesBuilder("owned-rec-series").Build()) + .Build(); + context.Library.Add(lib); + await context.SaveChangesAsync(); + + var series = context.Series.First(s => s.Name == "series0"); + var ownedRecSeries = context.Series.First(s => s.Name == "owned-rec-series"); + + var metadata = new ExternalSeriesMetadata + { + SeriesId = series.Id, + ValidUntilUtc = DateTime.UtcNow.AddDays(7), + ExternalRecommendations = new List + { + // owned — has a SeriesId pointing to an existing series + new() { SeriesId = ownedRecSeries.Id, Name = ownedRecSeries.Name, Provider = ScrobbleProvider.AniList, CoverUrl = string.Empty, Url = string.Empty}, + // external — no SeriesId (not in library) + new() { SeriesId = null, Name = "External Rec 1", Provider = ScrobbleProvider.AniList, CoverUrl = string.Empty, Url = string.Empty }, + new() { SeriesId = null, Name = "External Rec 2", Provider = ScrobbleProvider.AniList, CoverUrl = string.Empty, Url = string.Empty } + } + }; + context.ExternalSeriesMetadata.Add(metadata); + await context.SaveChangesAsync(); + + var result = await unitOfWork.ExternalSeriesMetadataRepository.GetSeriesDetailPlusDto(series.Id); + + Assert.NotNull(result); + Assert.NotNull(result.Recommendations); + + Assert.Single(result.Recommendations.OwnedSeries); + Assert.Equal(ownedRecSeries.Name, result.Recommendations.OwnedSeries[0].Name); + + Assert.Equal(2, result.Recommendations.ExternalSeries.Count()); + Assert.Contains(result.Recommendations.ExternalSeries, r => r.Name == "External Rec 1"); + Assert.Contains(result.Recommendations.ExternalSeries, r => r.Name == "External Rec 2"); + } + + [Fact] + public async Task GetSeriesDetailPlusDto_WithNoRatingsOrReviews_ReturnsEmptyCollections() + { + var (unitOfWork, context, _) = await CreateDatabase(); + + var lib = new LibraryBuilder("lib0") + .WithSeries(new SeriesBuilder("series0").Build()) + .Build(); + context.Library.Add(lib); + await context.SaveChangesAsync(); + + var series = context.Series.First(s => s.Name == "series0"); + + var metadata = new ExternalSeriesMetadata + { + SeriesId = series.Id, + ValidUntilUtc = DateTime.UtcNow.AddDays(7), + ExternalRatings = new List(), + ExternalReviews = new List(), + ExternalRecommendations = new List() + }; + context.ExternalSeriesMetadata.Add(metadata); + await context.SaveChangesAsync(); + + var result = await unitOfWork.ExternalSeriesMetadataRepository.GetSeriesDetailPlusDto(series.Id); + + Assert.NotNull(result); + Assert.Empty(result.Ratings ?? []); + Assert.Empty(result.Reviews); + Assert.Empty(result.Recommendations?.OwnedSeries ?? []); + Assert.Empty(result.Recommendations?.ExternalSeries ?? []); + } + + [Fact] + public async Task GetSeriesDetailPlusDto_OwnedRecommendations_AreSortedBySortNameAscending() + { + var (unitOfWork, context, _) = await CreateDatabase(); + + var lib = new LibraryBuilder("lib0") + .WithSeries(new SeriesBuilder("series0").Build()) + .WithSeries(new SeriesBuilder("Charlie").Build()) + .WithSeries(new SeriesBuilder("Alpha").Build()) + .WithSeries(new SeriesBuilder("Bravo").Build()) + .Build(); + context.Library.Add(lib); + await context.SaveChangesAsync(); + + var series = context.Series.First(s => s.Name == "series0"); + var charlie = context.Series.First(s => s.Name == "Charlie"); + var alpha = context.Series.First(s => s.Name == "Alpha"); + var bravo = context.Series.First(s => s.Name == "Bravo"); + + var metadata = new ExternalSeriesMetadata + { + SeriesId = series.Id, + ValidUntilUtc = DateTime.UtcNow.AddDays(7), + ExternalRecommendations = new List + { + new() { SeriesId = charlie.Id, Name = "Charlie", Provider = ScrobbleProvider.AniList, CoverUrl = string.Empty, Url = string.Empty }, + new() { SeriesId = alpha.Id, Name = "Alpha", Provider = ScrobbleProvider.AniList, CoverUrl = string.Empty, Url = string.Empty }, + new() { SeriesId = bravo.Id, Name = "Bravo", Provider = ScrobbleProvider.AniList, CoverUrl = string.Empty, Url = string.Empty } + } + }; + context.ExternalSeriesMetadata.Add(metadata); + await context.SaveChangesAsync(); + + var result = await unitOfWork.ExternalSeriesMetadataRepository.GetSeriesDetailPlusDto(series.Id); + + Assert.NotNull(result); + var owned = result.Recommendations?.OwnedSeries ?? []; + Assert.Equal(3, owned.Count); + Assert.Equal("Alpha", owned[0].Name); + Assert.Equal("Bravo", owned[1].Name); + Assert.Equal("Charlie", owned[2].Name); + } +} diff --git a/API.Tests/Repository/GenreRepositoryTests.cs b/Kavita.Database.Tests/Repositories/GenreRepositoryTests.cs similarity index 97% rename from API.Tests/Repository/GenreRepositoryTests.cs rename to Kavita.Database.Tests/Repositories/GenreRepositoryTests.cs index 5ad42809a..426640b92 100644 --- a/API.Tests/Repository/GenreRepositoryTests.cs +++ b/Kavita.Database.Tests/Repositories/GenreRepositoryTests.cs @@ -2,16 +2,17 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.DTOs.Metadata.Browse; -using API.Entities; -using API.Entities.Enums; -using API.Helpers; -using API.Helpers.Builders; +using Kavita.Common.Helpers; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.Metadata.Browse; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; using Xunit; using Xunit.Abstractions; -namespace API.Tests.Repository; +namespace Kavita.Database.Tests.Repositories; public class GenreRepositoryTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { diff --git a/API.Tests/Repository/PersonRepositoryTests.cs b/Kavita.Database.Tests/Repositories/PersonRepositoryTests.cs similarity index 95% rename from API.Tests/Repository/PersonRepositoryTests.cs rename to Kavita.Database.Tests/Repositories/PersonRepositoryTests.cs index b6297bfe2..a2003e429 100644 --- a/API.Tests/Repository/PersonRepositoryTests.cs +++ b/Kavita.Database.Tests/Repositories/PersonRepositoryTests.cs @@ -1,19 +1,21 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.DTOs.Metadata.Browse; -using API.DTOs.Metadata.Browse.Requests; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Person; -using API.Helpers; -using API.Helpers.Builders; +using Kavita.Common.Helpers; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.Metadata.Browse; +using Kavita.Models.DTOs.Metadata.Browse.Requests; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Person; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; using Xunit; using Xunit.Abstractions; -namespace API.Tests.Repository; +namespace Kavita.Database.Tests.Repositories; public class PersonRepositoryTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { @@ -237,19 +239,19 @@ public class PersonRepositoryTests(ITestOutputHelper outputHelper): AbstractDbTe var ageChapterRoles = await unitOfWork.PersonRepository.GetRolesForPersonByName(lib1ChapterAgePerson.Id, fullAccess.Id); Assert.Equal(3, sharedSeriesRoles.Count()); Assert.Equal(6, chapterRoles.Count()); - Assert.Single(ageChapterRoles); + Assert.Single((IEnumerable)ageChapterRoles); var restrictedRoles = await unitOfWork.PersonRepository.GetRolesForPersonByName(sharedSeriesPerson.Id, restrictedAccess.Id); var restrictedChapterRoles = await unitOfWork.PersonRepository.GetRolesForPersonByName(sharedChaptersPerson.Id, restrictedAccess.Id); var restrictedAgePersonChapterRoles = await unitOfWork.PersonRepository.GetRolesForPersonByName(lib1ChapterAgePerson.Id, restrictedAccess.Id); Assert.Equal(2, restrictedRoles.Count()); Assert.Equal(4, restrictedChapterRoles.Count()); - Assert.Single(restrictedAgePersonChapterRoles); + Assert.Single((IEnumerable)restrictedAgePersonChapterRoles); var restrictedAgeRoles = await unitOfWork.PersonRepository.GetRolesForPersonByName(sharedSeriesPerson.Id, restrictedAgeAccess.Id); var restrictedAgeChapterRoles = await unitOfWork.PersonRepository.GetRolesForPersonByName(sharedChaptersPerson.Id, restrictedAgeAccess.Id); var restrictedAgeAgePersonChapterRoles = await unitOfWork.PersonRepository.GetRolesForPersonByName(lib1ChapterAgePerson.Id, restrictedAgeAccess.Id); - Assert.Single(restrictedAgeRoles); + Assert.Single((IEnumerable)restrictedAgeRoles); Assert.Equal(2, restrictedAgeChapterRoles.Count()); // Note: There is a potential bug here where a person in a different chapter of an age restricted series will show up Assert.Empty(restrictedAgeAgePersonChapterRoles); @@ -294,10 +296,10 @@ public class PersonRepositoryTests(ITestOutputHelper outputHelper): AbstractDbTe Assert.Equal(2, series.Count()); series = await unitOfWork.PersonRepository.GetSeriesKnownFor(sharedSeriesPerson.Id, restrictedAgeAccess.Id); - Assert.Single(series); + Assert.Single((IEnumerable)series); series = await unitOfWork.PersonRepository.GetSeriesKnownFor(lib1SeriesPerson.Id, restrictedAgeAccess.Id); - Assert.Single(series); + Assert.Single((IEnumerable)series); } [Fact] @@ -312,7 +314,7 @@ public class PersonRepositoryTests(ITestOutputHelper outputHelper): AbstractDbTe var chapters = await unitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, fullAccess.Id, PersonRole.Colorist); var restrictedChapters = await unitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, restrictedAccess.Id, PersonRole.Colorist); var restrictedAgeChapters = await unitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, restrictedAgeAccess.Id, PersonRole.Colorist); - Assert.Single(chapters); + Assert.Single((IEnumerable)chapters); Assert.Empty(restrictedChapters); Assert.Empty(restrictedAgeChapters); @@ -320,16 +322,16 @@ public class PersonRepositoryTests(ITestOutputHelper outputHelper): AbstractDbTe chapters = await unitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, fullAccess.Id, PersonRole.Imprint); restrictedChapters = await unitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, restrictedAccess.Id, PersonRole.Imprint); restrictedAgeChapters = await unitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, restrictedAgeAccess.Id, PersonRole.Imprint); - Assert.Single(chapters); - Assert.Single(restrictedChapters); + Assert.Single((IEnumerable)chapters); + Assert.Single((IEnumerable)restrictedChapters); Assert.Empty(restrictedAgeChapters); // Lib1 - not age restricted series chapters = await unitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, fullAccess.Id, PersonRole.Team); restrictedChapters = await unitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, restrictedAccess.Id, PersonRole.Team); restrictedAgeChapters = await unitOfWork.PersonRepository.GetChaptersForPersonByRole(sharedChaptersPerson.Id, restrictedAgeAccess.Id, PersonRole.Team); - Assert.Single(chapters); - Assert.Single(restrictedChapters); - Assert.Single(restrictedAgeChapters); + Assert.Single((IEnumerable)chapters); + Assert.Single((IEnumerable)restrictedChapters); + Assert.Single((IEnumerable)restrictedAgeChapters); } } diff --git a/API.Tests/Repository/SeriesRepositoryTests.cs b/Kavita.Database.Tests/Repositories/SeriesRepositoryTests.cs similarity index 95% rename from API.Tests/Repository/SeriesRepositoryTests.cs rename to Kavita.Database.Tests/Repositories/SeriesRepositoryTests.cs index fd58badb0..4151e14aa 100644 --- a/API.Tests/Repository/SeriesRepositoryTests.cs +++ b/Kavita.Database.Tests/Repositories/SeriesRepositoryTests.cs @@ -1,13 +1,12 @@ using System.Threading.Tasks; -using API.Data; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Helpers.Builders; +using Kavita.API.Database; +using Kavita.Models.Builders; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; using Xunit; using Xunit.Abstractions; -namespace API.Tests.Repository; -#nullable enable +namespace Kavita.Database.Tests.Repositories; public class SeriesRepositoryTests(ITestOutputHelper testOutputHelper) : AbstractDbTest(testOutputHelper) { diff --git a/API.Tests/Repository/TagRepositoryTests.cs b/Kavita.Database.Tests/Repositories/TagRepositoryTests.cs similarity index 97% rename from API.Tests/Repository/TagRepositoryTests.cs rename to Kavita.Database.Tests/Repositories/TagRepositoryTests.cs index af4ac7cea..f1f747747 100644 --- a/API.Tests/Repository/TagRepositoryTests.cs +++ b/Kavita.Database.Tests/Repositories/TagRepositoryTests.cs @@ -2,17 +2,18 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.DTOs.Metadata.Browse; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Helpers; -using API.Helpers.Builders; +using Kavita.Common.Helpers; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.Metadata.Browse; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; using Xunit; using Xunit.Abstractions; -namespace API.Tests.Repository; +namespace Kavita.Database.Tests.Repositories; public class TagRepositoryTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { diff --git a/API/Helpers/Converters/AnnotationFilterFieldValueConverter.cs b/Kavita.Database/Converters/AnnotationFilterFieldValueConverter.cs similarity index 88% rename from API/Helpers/Converters/AnnotationFilterFieldValueConverter.cs rename to Kavita.Database/Converters/AnnotationFilterFieldValueConverter.cs index 86e188c5c..f3a3d9fc2 100644 --- a/API/Helpers/Converters/AnnotationFilterFieldValueConverter.cs +++ b/Kavita.Database/Converters/AnnotationFilterFieldValueConverter.cs @@ -1,8 +1,8 @@ using System; -using API.DTOs.Filtering.v2; -using API.Extensions; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs.Filtering.v2; -namespace API.Helpers.Converters; +namespace Kavita.Database.Converters; public static class AnnotationFilterFieldValueConverter { diff --git a/API/Helpers/Converters/FilterFieldValueConverter.cs b/Kavita.Database/Converters/FilterFieldValueConverter.cs similarity index 96% rename from API/Helpers/Converters/FilterFieldValueConverter.cs rename to Kavita.Database/Converters/FilterFieldValueConverter.cs index 4755392a9..31b01ce01 100644 --- a/API/Helpers/Converters/FilterFieldValueConverter.cs +++ b/Kavita.Database/Converters/FilterFieldValueConverter.cs @@ -1,12 +1,11 @@ -using System; +using System; using System.Globalization; using System.Linq; -using API.DTOs.Filtering.v2; -using API.Entities.Enums; -using API.Extensions; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.Entities.Enums; -namespace API.Helpers.Converters; -#nullable enable +namespace Kavita.Database.Converters; public static class FilterFieldValueConverter { diff --git a/API/Helpers/Converters/PersonFilterFieldValueConverter.cs b/Kavita.Database/Converters/PersonFilterFieldValueConverter.cs similarity index 80% rename from API/Helpers/Converters/PersonFilterFieldValueConverter.cs rename to Kavita.Database/Converters/PersonFilterFieldValueConverter.cs index 822ce105a..08ec45331 100644 --- a/API/Helpers/Converters/PersonFilterFieldValueConverter.cs +++ b/Kavita.Database/Converters/PersonFilterFieldValueConverter.cs @@ -1,10 +1,10 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; -using API.DTOs.Filtering.v2; -using API.Entities.Enums; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.Entities.Enums; -namespace API.Helpers.Converters; +namespace Kavita.Database.Converters; public static class PersonFilterFieldValueConverter { @@ -20,7 +20,7 @@ public static class PersonFilterFieldValueConverter }; } - private static IList ParsePersonRoles(string value) + private static List ParsePersonRoles(string value) { if (string.IsNullOrEmpty(value)) return []; diff --git a/API/Data/DataContext.cs b/Kavita.Database/DataContext.cs similarity index 97% rename from API/Data/DataContext.cs rename to Kavita.Database/DataContext.cs index 9f7d560c8..08046ab67 100644 --- a/API/Data/DataContext.cs +++ b/Kavita.Database/DataContext.cs @@ -1,35 +1,34 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using API.DTOs.Progress; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Enums.User; -using API.Entities.Enums.UserPreferences; -using API.Entities.History; -using API.Entities.Interfaces; -using API.Entities.Metadata; -using API.Entities.MetadataMatching; -using API.Entities.Person; -using API.Entities.Progress; -using API.Entities.Scrobble; -using API.Entities.User; -using API.Extensions; -using Hangfire.Storage.SQLite.Entities; +using Kavita.API.Database; +using Kavita.Database.Extensions; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.User; +using Kavita.Models.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.History; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.MetadataMatching; +using Kavita.Models.Entities.Person; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.Scrobble; +using Kavita.Models.Entities.User; using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; -namespace API.Data; +namespace Kavita.Database; public sealed class DataContext : IdentityDbContext, AppUserRole, IdentityUserLogin, - IdentityRoleClaim, IdentityUserToken>, IDataProtectionKeyContext + IdentityRoleClaim, IdentityUserToken>, IDataProtectionKeyContext, IDataContext { public DataContext(DbContextOptions options) : base(options) { @@ -105,7 +104,6 @@ public sealed class DataContext : IdentityDbContext() .HasOne(pt => pt.Series) .WithMany(p => p.Relations) diff --git a/Kavita.Database/Extensions/ApplicationServiceExtensions.cs b/Kavita.Database/Extensions/ApplicationServiceExtensions.cs new file mode 100644 index 000000000..618c041aa --- /dev/null +++ b/Kavita.Database/Extensions/ApplicationServiceExtensions.cs @@ -0,0 +1,42 @@ +using Kavita.API.Database; +using Kavita.Common.EnvironmentInfo; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.Extensions.DependencyInjection; +using NeoSmart.Caching.Sqlite; + +namespace Kavita.Database.Extensions; + +public static class ApplicationServiceExtensions +{ + public static void AddKavitaDatabases(this IServiceCollection services) + { + services.AddSqLite(); + + services.AddScoped(); + services.AddScoped(); + + // Store keys inside database, such that cookies can be decrypted between container restarts + services.AddDataProtection() + .PersistKeysToDbContext() + .SetApplicationName(BuildInfo.AppName); + } + + private static void AddSqLite(this IServiceCollection services) + { + services.AddSqliteCache("config/cache.db"); + + services.AddDbContextPool(options => + { + options.UseSqlite("Data source=config/kavita.db", builder => + { + builder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); + }); + options.EnableDetailedErrors(); + options.EnableSensitiveDataLogging(); + options.ConfigureWarnings(warnings => + warnings.Ignore(RelationalEventId.PendingModelChangesWarning)); + }); + } +} diff --git a/API/Extensions/QueryExtensions/AuthKeyQueryExtensions.cs b/Kavita.Database/Extensions/AuthKeyQueryExtensions.cs similarity index 76% rename from API/Extensions/QueryExtensions/AuthKeyQueryExtensions.cs rename to Kavita.Database/Extensions/AuthKeyQueryExtensions.cs index a6954323b..85e847439 100644 --- a/API/Extensions/QueryExtensions/AuthKeyQueryExtensions.cs +++ b/Kavita.Database/Extensions/AuthKeyQueryExtensions.cs @@ -1,8 +1,8 @@ -using System; +using System; using System.Linq; -using API.Entities.User; +using Kavita.Models.Entities.User; -namespace API.Extensions.QueryExtensions; +namespace Kavita.Database.Extensions; public static class AuthKeyQueryExtensions { @@ -11,3 +11,4 @@ public static class AuthKeyQueryExtensions return queryable.Where(k => k.ExpiresAtUtc == null || k.ExpiresAtUtc > DateTime.UtcNow); } } + diff --git a/API/Extensions/QueryExtensions/Filtering/BookmarkSort.cs b/Kavita.Database/Extensions/BookmarkSortExtensions.cs similarity index 84% rename from API/Extensions/QueryExtensions/Filtering/BookmarkSort.cs rename to Kavita.Database/Extensions/BookmarkSortExtensions.cs index 030517dbf..88ab6af4a 100644 --- a/API/Extensions/QueryExtensions/Filtering/BookmarkSort.cs +++ b/Kavita.Database/Extensions/BookmarkSortExtensions.cs @@ -1,18 +1,12 @@ -using System.Linq; -using API.DTOs.Filtering; -using API.Entities; +using System.Linq; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.Entities; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; -namespace API.Extensions.QueryExtensions.Filtering; -#nullable enable +namespace Kavita.Database.Extensions; -public class BookmarkSeriesPair -{ - public AppUserBookmark Bookmark { get; init; } = null!; - public Series Series { get; init; } = null!; -} - -public static class BookmarkSort +public static class BookmarkSortExtensions { /// /// Applies the correct sort based on diff --git a/API/Extensions/QueryExtensions/ChapterQueryExtensions.cs b/Kavita.Database/Extensions/ChapterQueryExtensions.cs similarity index 61% rename from API/Extensions/QueryExtensions/ChapterQueryExtensions.cs rename to Kavita.Database/Extensions/ChapterQueryExtensions.cs index 44a2f2a68..256168c10 100644 --- a/API/Extensions/QueryExtensions/ChapterQueryExtensions.cs +++ b/Kavita.Database/Extensions/ChapterQueryExtensions.cs @@ -1,9 +1,10 @@ -using System.Linq; -using API.Entities; -using API.Services.Tasks.Scanner.Parser; +using System.Linq; +using Kavita.Common.Constants; +using Kavita.Models.Constants; +using Kavita.Models.Entities; using Microsoft.EntityFrameworkCore; -namespace API.Extensions.QueryExtensions; +namespace Kavita.Database.Extensions; public static class ChapterQueryExtensions { @@ -13,11 +14,11 @@ public static class ChapterQueryExtensions .Include(c => c.Volume) .OrderBy(c => // Priority 1: Regular volumes (not loose-leaf, not special) - c.Volume.MinNumber == Parser.LooseLeafVolumeNumber || - c.Volume.MinNumber == Parser.SpecialVolumeNumber ? 1 : 0) + c.Volume.MinNumber == ParserConstants.LooseLeafVolumeNumber || + c.Volume.MinNumber == ParserConstants.SpecialVolumeNumber ? 1 : 0) .ThenBy(c => // Priority 2: Loose leaf over specials - c.Volume.MinNumber == Parser.SpecialVolumeNumber ? 1 : 0) + c.Volume.MinNumber == ParserConstants.SpecialVolumeNumber ? 1 : 0) // Priority 3: Non-special chapters .ThenBy(c => c.IsSpecial ? 1 : 0) .ThenBy(c => c.Volume.MinNumber) diff --git a/API/Extensions/DataContextExtensions.cs b/Kavita.Database/Extensions/DataContextExtensions.cs similarity index 92% rename from API/Extensions/DataContextExtensions.cs rename to Kavita.Database/Extensions/DataContextExtensions.cs index abf076861..d58660b7b 100644 --- a/API/Extensions/DataContextExtensions.cs +++ b/Kavita.Database/Extensions/DataContextExtensions.cs @@ -1,11 +1,10 @@ using System.Text.Json; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace API.Extensions; +namespace Kavita.Database.Extensions; public static class DataContextExtensions { - public static PropertyBuilder HasJsonConversion(this PropertyBuilder builder, TProperty def = default) { return builder.HasConversion( @@ -13,5 +12,4 @@ public static class DataContextExtensions v => JsonSerializer.Deserialize(v, JsonSerializerOptions.Default) ?? def ); } - } diff --git a/API/Extensions/QueryExtensions/Filtering/ActivityFilter.cs b/Kavita.Database/Extensions/Filters/ActivityFilter.cs similarity index 92% rename from API/Extensions/QueryExtensions/Filtering/ActivityFilter.cs rename to Kavita.Database/Extensions/Filters/ActivityFilter.cs index d2c8f9641..cfadd107b 100644 --- a/API/Extensions/QueryExtensions/Filtering/ActivityFilter.cs +++ b/Kavita.Database/Extensions/Filters/ActivityFilter.cs @@ -1,12 +1,11 @@ using System.Linq; -using API.DTOs.Statistics; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Enums.UserPreferences; -using API.Entities.Progress; -using API.Entities.User; +using Kavita.Models.DTOs.Statistics; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; -namespace API.Extensions.QueryExtensions.Filtering; +namespace Kavita.Database.Extensions.Filters; public static class ActivityFilter { diff --git a/Kavita.Database/Extensions/Filters/AnnotationFilter.cs b/Kavita.Database/Extensions/Filters/AnnotationFilter.cs new file mode 100644 index 000000000..311008e93 --- /dev/null +++ b/Kavita.Database/Extensions/Filters/AnnotationFilter.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Kavita.Common; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.Entities.User; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Extensions.Filters; + +public static class AnnotationFilter +{ + + extension(IQueryable queryable) + { + public IQueryable IsOwnedBy(bool condition, + FilterComparison comparison, IList ownerIds) + { + if (ownerIds.Count == 0 || !condition) return queryable; + + return comparison switch + { + FilterComparison.Equal => queryable.Where(a => a.AppUserId == ownerIds[0]), + FilterComparison.Contains => queryable.Where(a => ownerIds.Contains(a.AppUserId)), + FilterComparison.NotContains => queryable.Where(a => !ownerIds.Contains(a.AppUserId)), + FilterComparison.NotEqual => queryable.Where(a => a.AppUserId != ownerIds[0]), + _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), + }; + } + + public IQueryable IsInLibrary(bool condition, + FilterComparison comparison, IList libraryIds) + { + if (libraryIds.Count == 0 || !condition) return queryable; + + return comparison switch + { + FilterComparison.Equal => queryable.Where(a => a.LibraryId == libraryIds[0]), + FilterComparison.Contains => queryable.Where(a => libraryIds.Contains(a.LibraryId)), + FilterComparison.NotContains => queryable.Where(a => !libraryIds.Contains(a.LibraryId)), + FilterComparison.NotEqual => queryable.Where(a => a.LibraryId != libraryIds[0]), + _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), + }; + } + + public IQueryable HasSeries(bool condition, + FilterComparison comparison, IList seriesIds) + { + if (seriesIds.Count == 0 || !condition) return queryable; + + return comparison switch + { + FilterComparison.Equal => queryable.Where(a => a.SeriesId == seriesIds[0]), + FilterComparison.Contains => queryable.Where(a => seriesIds.Contains(a.SeriesId)), + FilterComparison.NotContains => queryable.Where(a => !seriesIds.Contains(a.SeriesId)), + FilterComparison.NotEqual => queryable.Where(a => a.SeriesId != seriesIds[0]), + _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), + }; + } + + public IQueryable IsUsingHighlights(bool condition, + FilterComparison comparison, IList highlightSlotIdxs) + { + if (highlightSlotIdxs.Count == 0 || !condition) return queryable; + + return comparison switch + { + FilterComparison.Equal => queryable.Where(a => a.SelectedSlotIndex == highlightSlotIdxs[0]), + FilterComparison.Contains => queryable.Where(a => highlightSlotIdxs.Contains(a.SelectedSlotIndex)), + FilterComparison.NotContains => queryable.Where(a => !highlightSlotIdxs.Contains(a.SelectedSlotIndex)), + FilterComparison.NotEqual => queryable.Where(a => a.SelectedSlotIndex != highlightSlotIdxs[0]), + _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), + }; + } + + public IQueryable HasSelected(bool condition, + FilterComparison comparison, string value) + { + if (string.IsNullOrEmpty(value) || !condition) return queryable; + + return comparison switch + { + FilterComparison.Equal => queryable.Where(a => a.SelectedText == value), + FilterComparison.NotEqual => queryable.Where(a => a.SelectedText != value), + FilterComparison.BeginsWith => queryable.Where(a => EF.Functions.Like(a.SelectedText, $"{value}%")), + FilterComparison.EndsWith => queryable.Where(a => EF.Functions.Like(a.SelectedText, $"%{value}")), + FilterComparison.Matches => queryable.Where(a => EF.Functions.Like(a.SelectedText, $"%{value}%")), + FilterComparison.GreaterThan or + FilterComparison.GreaterThanEqual or + FilterComparison.LessThan or + FilterComparison.LessThanEqual or + FilterComparison.Contains or + FilterComparison.MustContains or + FilterComparison.NotContains or + FilterComparison.IsBefore or + FilterComparison.IsAfter or + FilterComparison.IsInLast or + FilterComparison.IsNotInLast or + FilterComparison.IsEmpty => throw new KavitaException($"{comparison} is not applicable for Annotation.SelectedText"), + _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), + }; + } + + public IQueryable HasCommented(bool condition, + FilterComparison comparison, string value) + { + if (string.IsNullOrEmpty(value) || !condition) return queryable; + + return comparison switch + { + FilterComparison.Equal => queryable.Where(a => a.CommentPlainText == value), + FilterComparison.NotEqual => queryable.Where(a => a.CommentPlainText != value), + FilterComparison.BeginsWith => queryable.Where(a => EF.Functions.Like(a.CommentPlainText, $"{value}%")), + FilterComparison.EndsWith => queryable.Where(a => EF.Functions.Like(a.CommentPlainText, $"%{value}")), + FilterComparison.Matches => queryable.Where(a => EF.Functions.Like(a.CommentPlainText, $"%{value}%")), + FilterComparison.GreaterThan or + FilterComparison.GreaterThanEqual or + FilterComparison.LessThan or + FilterComparison.LessThanEqual or + FilterComparison.Contains or + FilterComparison.MustContains or + FilterComparison.NotContains or + FilterComparison.IsBefore or + FilterComparison.IsAfter or + FilterComparison.IsInLast or + FilterComparison.IsNotInLast or + FilterComparison.IsEmpty => throw new KavitaException($"{comparison} is not applicable for Annotation.CommentPlainText"), + _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), + }; + } + + public IQueryable HasLikes(bool condition, + FilterComparison comparison, int value) + { + if (!condition) return queryable; + + return comparison switch + { + FilterComparison.Equal => queryable.Where(a => a.Likes.Count == value), + FilterComparison.NotEqual => queryable.Where(a => a.Likes.Count != value), + FilterComparison.GreaterThan => queryable.Where(a => a.Likes.Count > value), + FilterComparison.GreaterThanEqual => queryable.Where(a => a.Likes.Count >= value), + FilterComparison.LessThan => queryable.Where(a => a.Likes.Count < value), + FilterComparison.LessThanEqual => queryable.Where(a => a.Likes.Count <= value), + FilterComparison.BeginsWith or + FilterComparison.EndsWith or + FilterComparison.Matches or + FilterComparison.Contains or + FilterComparison.MustContains or + FilterComparison.NotContains or + FilterComparison.IsBefore or + FilterComparison.IsAfter or + FilterComparison.IsInLast or + FilterComparison.IsNotInLast or + FilterComparison.IsEmpty => throw new KavitaException($"{comparison} is not applicable for Annotation.Likes"), + _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), + }; + } + + public IQueryable IsLikedBy(bool condition, + FilterComparison comparison, IList value) + { + if (value.Count == 0 || !condition) return queryable; + + return comparison switch + { + FilterComparison.Equal => queryable.Where(a => a.Likes.Contains(value[0])), + FilterComparison.NotEqual => queryable.Where(a => a!.Likes.Contains(value[0])), + FilterComparison.Contains => queryable.Where(a => a.Likes.Any(value.Contains)), + FilterComparison.NotContains => queryable.Where(a => !a.Likes.Any(value.Contains)), + FilterComparison.GreaterThan or + FilterComparison.GreaterThanEqual or + FilterComparison.LessThan or + FilterComparison.LessThanEqual or + FilterComparison.BeginsWith or + FilterComparison.EndsWith or + FilterComparison.Matches or + FilterComparison.MustContains or + FilterComparison.IsBefore or + FilterComparison.IsAfter or + FilterComparison.IsInLast or + FilterComparison.IsNotInLast or + FilterComparison.IsEmpty => throw new KavitaException($"{comparison} is not applicable for Annotation.Likes"), + _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null), + }; + } + } +} diff --git a/API/Extensions/QueryExtensions/Filtering/PersonFilter.cs b/Kavita.Database/Extensions/Filters/PersonFilter.cs similarity index 97% rename from API/Extensions/QueryExtensions/Filtering/PersonFilter.cs rename to Kavita.Database/Extensions/Filters/PersonFilter.cs index c36164d9d..ba447c48b 100644 --- a/API/Extensions/QueryExtensions/Filtering/PersonFilter.cs +++ b/Kavita.Database/Extensions/Filters/PersonFilter.cs @@ -1,13 +1,13 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; -using API.DTOs.Filtering.v2; -using API.Entities.Enums; -using API.Entities.Person; using Kavita.Common; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Person; using Microsoft.EntityFrameworkCore; -namespace API.Extensions.QueryExtensions.Filtering; +namespace Kavita.Database.Extensions.Filters; public static class PersonFilter { diff --git a/Kavita.Database/Extensions/Filters/SeriesFilter.cs b/Kavita.Database/Extensions/Filters/SeriesFilter.cs new file mode 100644 index 000000000..f31b2815b --- /dev/null +++ b/Kavita.Database/Extensions/Filters/SeriesFilter.cs @@ -0,0 +1,944 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Extensions.Filters; + +public static class SeriesFilter +{ + private const float FloatingPointTolerance = 0.001f; + + extension(IQueryable queryable) + { + public IQueryable HasLanguage(bool condition, + FilterComparison comparison, IList languages) + { + if (languages.Count == 0 || !condition) return queryable; + + switch (comparison) + { + case FilterComparison.Equal: + return queryable.Where(s => s.Metadata.Language.Equals(languages[0])); + case FilterComparison.Contains: + return queryable.Where(s => languages.Contains(s.Metadata.Language)); + case FilterComparison.MustContains: + return queryable.Where(s => languages.All(s2 => s2.Equals(s.Metadata.Language))); + case FilterComparison.NotContains: + return queryable.Where(s => !languages.Contains(s.Metadata.Language)); + case FilterComparison.NotEqual: + return queryable.Where(s => !s.Metadata.Language.Equals(languages[0])); + case FilterComparison.Matches: + return queryable.Where(s => EF.Functions.Like(s.Metadata.Language, $"{languages[0]}%")); + case FilterComparison.GreaterThan: + case FilterComparison.GreaterThanEqual: + case FilterComparison.LessThan: + case FilterComparison.LessThanEqual: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.IsEmpty: + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + } + + public IQueryable HasReleaseYear(bool condition, + FilterComparison comparison, int? releaseYear) + { + if (!condition || releaseYear == null) return queryable; + + switch (comparison) + { + case FilterComparison.Equal: + return queryable.Where(s => s.Metadata.ReleaseYear == releaseYear); + case FilterComparison.GreaterThan: + case FilterComparison.IsAfter: + return queryable.Where(s => s.Metadata.ReleaseYear > releaseYear); + case FilterComparison.GreaterThanEqual: + return queryable.Where(s => s.Metadata.ReleaseYear >= releaseYear); + case FilterComparison.LessThan: + case FilterComparison.IsBefore: + return queryable.Where(s => s.Metadata.ReleaseYear < releaseYear); + case FilterComparison.LessThanEqual: + return queryable.Where(s => s.Metadata.ReleaseYear <= releaseYear); + case FilterComparison.IsInLast: + return queryable.Where(s => s.Metadata.ReleaseYear >= DateTime.Now.Year - (int) releaseYear); + case FilterComparison.IsNotInLast: + return queryable.Where(s => s.Metadata.ReleaseYear < DateTime.Now.Year - (int) releaseYear); + case FilterComparison.IsEmpty: + return queryable.Where(s => s.Metadata.ReleaseYear == 0); + case FilterComparison.Matches: + case FilterComparison.Contains: + case FilterComparison.NotContains: + case FilterComparison.NotEqual: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.MustContains: + throw new KavitaException($"{comparison} not applicable for Series.ReleaseYear"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + } + + public IQueryable HasRating(bool condition, + FilterComparison comparison, float rating, int userId) + { + if (rating < 0 || !condition || userId <= 0) return queryable; + + // AppUserRating stores a 5-digit number. + rating = Math.Clamp(rating, 0f, 5f); + + + switch (comparison) + { + case FilterComparison.Equal: + return queryable.Where(s => s.Ratings.Any(r => Math.Abs(r.Rating - rating) <= FloatingPointTolerance && r.AppUserId == userId)); + case FilterComparison.GreaterThan: + return queryable.Where(s => s.Ratings.Any(r => r.Rating > rating && r.AppUserId == userId)); + case FilterComparison.GreaterThanEqual: + return queryable.Where(s => s.Ratings.Any(r => r.Rating >= rating && r.AppUserId == userId)); + case FilterComparison.LessThan: + return queryable.Where(s => s.Ratings.Any(r => r.Rating < rating && r.AppUserId == userId)); + case FilterComparison.LessThanEqual: + return queryable.Where(s => s.Ratings.Any(r => r.Rating <= rating && r.AppUserId == userId)); + case FilterComparison.NotEqual: + return queryable.Where(s => s.Ratings.Any(r => Math.Abs(r.Rating - rating) >= FloatingPointTolerance && r.AppUserId == userId)); + case FilterComparison.IsEmpty: + return queryable.Where(s => s.Ratings.All(r => r.AppUserId != userId)); + case FilterComparison.Contains: + case FilterComparison.Matches: + 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.Rating"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + } + + public IQueryable HasAgeRating(bool condition, + FilterComparison comparison, IList ratings) + { + if (!condition || ratings.Count == 0) return queryable; + + var firstRating = ratings[0]; + switch (comparison) + { + case FilterComparison.Equal: + return queryable.Where(s => s.Metadata.AgeRating == firstRating); + case FilterComparison.GreaterThan: + return queryable.Where(s => s.Metadata.AgeRating > firstRating); + case FilterComparison.GreaterThanEqual: + return queryable.Where(s => s.Metadata.AgeRating >= firstRating); + case FilterComparison.LessThan: + return queryable.Where(s => s.Metadata.AgeRating < firstRating); + case FilterComparison.LessThanEqual: + return queryable.Where(s => s.Metadata.AgeRating <= firstRating); + case FilterComparison.Contains: + return queryable.Where(s => ratings.Contains(s.Metadata.AgeRating)); + case FilterComparison.NotContains: + return queryable.Where(s => !ratings.Contains(s.Metadata.AgeRating)); + case FilterComparison.NotEqual: + return queryable.Where(s => s.Metadata.AgeRating != firstRating); + case FilterComparison.Matches: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: + case FilterComparison.IsEmpty: + throw new KavitaException($"{comparison} not applicable for Series.AgeRating"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + } + + public IQueryable HasAverageReadTime(bool condition, + FilterComparison comparison, int avgReadTime) + { + if (!condition || avgReadTime < 0) return queryable; + + switch (comparison) + { + case FilterComparison.NotEqual: + return queryable.WhereNotEqual(s => s.AvgHoursToRead, avgReadTime); + case FilterComparison.Equal: + return queryable.WhereEqual(s => s.AvgHoursToRead, avgReadTime); + case FilterComparison.GreaterThan: + return queryable.WhereGreaterThan(s => s.AvgHoursToRead, avgReadTime); + case FilterComparison.GreaterThanEqual: + return queryable.WhereGreaterThanOrEqual(s => s.AvgHoursToRead, avgReadTime); + case FilterComparison.LessThan: + return queryable.WhereLessThan(s => s.AvgHoursToRead, avgReadTime); + case FilterComparison.LessThanEqual: + return queryable.WhereLessThanOrEqual(s => s.AvgHoursToRead, avgReadTime); + case FilterComparison.Contains: + case FilterComparison.Matches: + case FilterComparison.NotContains: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: + case FilterComparison.IsEmpty: + throw new KavitaException($"{comparison} not applicable for Series.AverageReadTime"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + } + + public IQueryable HasPublicationStatus(bool condition, + FilterComparison comparison, IList pubStatues) + { + if (!condition || pubStatues.Count == 0) return queryable; + + var firstStatus = pubStatues[0]; + switch (comparison) + { + case FilterComparison.Equal: + return queryable.Where(s => s.Metadata.PublicationStatus == firstStatus); + case FilterComparison.Contains: + return queryable.Where(s => pubStatues.Contains(s.Metadata.PublicationStatus)); + case FilterComparison.NotContains: + return queryable.Where(s => !pubStatues.Contains(s.Metadata.PublicationStatus)); + case FilterComparison.NotEqual: + return queryable.Where(s => s.Metadata.PublicationStatus != firstStatus); + case FilterComparison.MustContains: + case FilterComparison.GreaterThan: + case FilterComparison.GreaterThanEqual: + case FilterComparison.LessThan: + case FilterComparison.LessThanEqual: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.Matches: + case FilterComparison.IsEmpty: + throw new KavitaException($"{comparison} not applicable for Series.PublicationStatus"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + } + + /// + /// + /// + /// This is more taxing on memory as the percentage calculation must be done in Memory + /// + /// + public IQueryable HasReadingProgress(bool condition, + FilterComparison comparison, float readProgress, int userId) + { + if (!condition) return queryable; + + var subQuery = queryable + .Select(s => new + { + SeriesId = s.Id, + SeriesName = s.Name, + Percentage = s.Progress + .Where(p => p != null && p.AppUserId == userId) + .Sum(p => p != null ? (p.PagesRead * 1.0f / s.Pages) : 0f) * 100f + }) + .AsSplitQuery(); + + switch (comparison) + { + case FilterComparison.Equal: + subQuery = subQuery.WhereEqual(s => s.Percentage, readProgress); + break; + case FilterComparison.GreaterThan: + subQuery = subQuery.WhereGreaterThan(s => s.Percentage, readProgress); + break; + case FilterComparison.GreaterThanEqual: + subQuery = subQuery.WhereGreaterThanOrEqual(s => s.Percentage, readProgress); + break; + case FilterComparison.LessThan: + subQuery = subQuery.WhereLessThan(s => s.Percentage, readProgress); + break; + case FilterComparison.LessThanEqual: + subQuery = subQuery.WhereLessThanOrEqual(s => s.Percentage, readProgress); + break; + case FilterComparison.NotEqual: + subQuery = subQuery.WhereNotEqual(s => s.Percentage, readProgress); + break; + case FilterComparison.IsEmpty: + 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.ReadProgress"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + + var ids = subQuery.Select(s => s.SeriesId); + return queryable.Where(s => ids.Contains(s.Id)); + } + + public IQueryable HasAverageRating(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 + { + SeriesId = s.Id, + SeriesName = s.Name, + AverageRating = s.ExternalSeriesMetadata.AverageExternalRating + }) + .AsSplitQuery() + .AsQueryable(); + + switch (comparison) + { + case FilterComparison.Equal: + subQuery = subQuery.WhereEqual(s => s.AverageRating, rating); + break; + case FilterComparison.GreaterThan: + subQuery = subQuery.WhereGreaterThan(s => s.AverageRating, rating); + break; + case FilterComparison.GreaterThanEqual: + subQuery = subQuery.WhereGreaterThanOrEqual(s => s.AverageRating, rating); + break; + case FilterComparison.LessThan: + subQuery = subQuery.WhereLessThan(s => s.AverageRating, rating); + break; + case FilterComparison.LessThanEqual: + subQuery = subQuery.WhereLessThanOrEqual(s => s.AverageRating, rating); + break; + case FilterComparison.NotEqual: + subQuery = subQuery.WhereNotEqual(s => s.AverageRating, rating); + 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: + case FilterComparison.IsEmpty: + throw new KavitaException($"{comparison} not applicable for Series.AverageRating"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + + var ids = subQuery.Select(s => s.SeriesId); + return queryable.Where(s => ids.Contains(s.Id)); + } + + /// + /// HasReadingDate but used to filter where last reading point was TODAY() - timeDeltaDays. This allows the user + /// to build smart filters "Haven't read in a month" + /// + public IQueryable HasReadLast(bool condition, + FilterComparison comparison, int timeDeltaDays, int userId) + { + if (!condition || timeDeltaDays == 0) return queryable; + + var subQuery = queryable + .Include(s => s.Progress) + .Where(s => s.Progress.Any()) + .Select(s => new + { + SeriesId = s.Id, + SeriesName = s.Name, + MaxDate = s.Progress.Where(p => p != null && p.AppUserId == userId) + .Select(p => (DateTime?) p.LastModified) + .DefaultIfEmpty() + .Max() + }) + .Where(s => s.MaxDate != null) + .AsSplitQuery() + .AsEnumerable(); + + var date = DateTime.Now.AddDays(-timeDeltaDays); + + switch (comparison) + { + case FilterComparison.Equal: + subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate.Equals(date)); + break; + case FilterComparison.IsAfter: + case FilterComparison.GreaterThan: + subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate > date); + break; + case FilterComparison.GreaterThanEqual: + subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate >= date); + break; + case FilterComparison.IsBefore: + case FilterComparison.LessThan: + subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate < date); + break; + case FilterComparison.LessThanEqual: + subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate <= date); + break; + case FilterComparison.NotEqual: + subQuery = subQuery.Where(s => s.MaxDate != null && !s.MaxDate.Equals(date)); + break; + case FilterComparison.Matches: + case FilterComparison.Contains: + case FilterComparison.NotContains: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: + case FilterComparison.IsEmpty: + throw new KavitaException($"{comparison} not applicable for Series.ReadProgress"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + + var ids = subQuery.Select(s => s.SeriesId); + return queryable.Where(s => ids.Contains(s.Id)); + } + + public IQueryable HasReadingDate(bool condition, + FilterComparison comparison, DateTime? date, int userId) + { + if (!condition || !date.HasValue) return queryable; + + var subQuery = queryable + .Include(s => s.Progress) + .Where(s => s.Progress.Any()) + .Select(s => new + { + SeriesId = s.Id, + SeriesName = s.Name, + MaxDate = s.Progress.Where(p => p != null && p.AppUserId == userId) + .Select(p => (DateTime?) p.LastModified) + .DefaultIfEmpty() + .Max() + }) + .Where(s => s.MaxDate != null) + .AsSplitQuery() + .AsEnumerable(); + + switch (comparison) + { + case FilterComparison.Equal: + subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate.Equals(date)); + break; + case FilterComparison.IsAfter: + case FilterComparison.GreaterThan: + subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate > date); + break; + case FilterComparison.GreaterThanEqual: + subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate >= date); + break; + case FilterComparison.IsBefore: + case FilterComparison.LessThan: + subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate < date); + break; + case FilterComparison.LessThanEqual: + subQuery = subQuery.Where(s => s.MaxDate != null && s.MaxDate <= date); + break; + case FilterComparison.NotEqual: + subQuery = subQuery.Where(s => s.MaxDate != null && !s.MaxDate.Equals(date)); + break; + case FilterComparison.Matches: + case FilterComparison.Contains: + case FilterComparison.NotContains: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: + case FilterComparison.IsEmpty: + throw new KavitaException($"{comparison} not applicable for Series.ReadProgress"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + + var ids = subQuery.Select(s => s.SeriesId); + return queryable.Where(s => ids.Contains(s.Id)); + } + + public IQueryable HasTags(bool condition, + FilterComparison comparison, IList tags) + { + if (!condition || (comparison != FilterComparison.IsEmpty && tags.Count == 0)) return queryable; + + switch (comparison) + { + case FilterComparison.Equal: + case FilterComparison.Contains: + return queryable.Where(s => s.Metadata.Tags.Any(t => tags.Contains(t.Id))); + case FilterComparison.NotEqual: + case FilterComparison.NotContains: + return queryable.Where(s => s.Metadata.Tags.All(t => !tags.Contains(t.Id))); + case FilterComparison.MustContains: + // Deconstruct and do a Union of a bunch of where statements since this doesn't translate + var queries = new List>() + { + queryable + }; + queries.AddRange(tags.Select(gId => queryable.Where(s => s.Metadata.Tags.Any(p => p.Id == gId)))); + + return queries.Aggregate((q1, q2) => q1.Intersect(q2)); + case FilterComparison.IsEmpty: + return queryable.Where(s => s.Metadata.Tags.Count == 0); + case FilterComparison.GreaterThan: + case FilterComparison.GreaterThanEqual: + case FilterComparison.LessThan: + case FilterComparison.LessThanEqual: + case FilterComparison.Matches: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + throw new KavitaException($"{comparison} not applicable for Series.Tags"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + } + + public IQueryable HasPeople(bool condition, + FilterComparison comparison, IList people, PersonRole role) + { + if (!condition || (comparison != FilterComparison.IsEmpty && people.Count == 0)) return queryable; + + switch (comparison) + { + case FilterComparison.Equal: + case FilterComparison.Contains: + return queryable.Where(s => s.Metadata.People.Any(p => people.Contains(p.PersonId) && p.Role == role)); + case FilterComparison.NotEqual: + case FilterComparison.NotContains: + return queryable.Where(s => s.Metadata.People.All(p => !people.Contains(p.PersonId) || p.Role != role)); + case FilterComparison.MustContains: + var queries = new List>() + { + queryable + }; + queries.AddRange(people.Select(personId => + queryable.Where(s => s.Metadata.People.Any(p => p.PersonId == personId && p.Role == role)))); + + return queries.Aggregate((q1, q2) => q1.Intersect(q2)); + case FilterComparison.IsEmpty: + // Ensure no person with the given role exists + return queryable.Where(s => s.Metadata.People.All(p => p.Role != role)); + case FilterComparison.GreaterThan: + case FilterComparison.GreaterThanEqual: + case FilterComparison.LessThan: + case FilterComparison.LessThanEqual: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.Matches: + throw new KavitaException($"{comparison} not applicable for Series.People"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + } + + public IQueryable HasPeopleLegacy(bool condition, + FilterComparison comparison, IList people) + { + if (!condition || people.Count == 0) return queryable; + + switch (comparison) + { + case FilterComparison.Equal: + case FilterComparison.Contains: + return queryable.Where(s => s.Metadata.People.Any(p => people.Contains(p.PersonId))); + case FilterComparison.NotEqual: + case FilterComparison.NotContains: + return queryable.Where(s => s.Metadata.People.All(t => !people.Contains(t.PersonId))); + case FilterComparison.MustContains: + // Deconstruct and do a Union of a bunch of where statements since this doesn't translate + var queries = new List>() + { + queryable + }; + queries.AddRange(people.Select(gId => queryable.Where(s => s.Metadata.People.Any(p => p.PersonId == gId)))); + + return queries.Aggregate((q1, q2) => q1.Intersect(q2)); + case FilterComparison.IsEmpty: + case FilterComparison.GreaterThan: + case FilterComparison.GreaterThanEqual: + case FilterComparison.LessThan: + case FilterComparison.LessThanEqual: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.Matches: + throw new KavitaException($"{comparison} not applicable for Series.People"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + } + + public IQueryable HasGenre(bool condition, + FilterComparison comparison, IList genres) + { + if (!condition || (comparison != FilterComparison.IsEmpty && genres.Count == 0)) return queryable; + + switch (comparison) + { + case FilterComparison.Equal: + case FilterComparison.Contains: + return queryable.Where(s => s.Metadata.Genres.Any(p => genres.Contains(p.Id))); + case FilterComparison.NotEqual: + case FilterComparison.NotContains: + return queryable.Where(s => s.Metadata.Genres.All(p => !genres.Contains(p.Id))); + case FilterComparison.MustContains: + // Deconstruct and do a Union of a bunch of where statements since this doesn't translate + var queries = new List>() + { + queryable + }; + queries.AddRange(genres.Select(gId => queryable.Where(s => s.Metadata.Genres.Any(p => p.Id == gId)))); + + return queries.Aggregate((q1, q2) => q1.Intersect(q2)); + case FilterComparison.IsEmpty: + return queryable.Where(s => s.Metadata.Genres.Count == 0); + case FilterComparison.GreaterThan: + case FilterComparison.GreaterThanEqual: + case FilterComparison.LessThan: + case FilterComparison.LessThanEqual: + case FilterComparison.Matches: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + throw new KavitaException($"{comparison} not applicable for Series.Genres"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + } + + public IQueryable HasFormat(bool condition, + FilterComparison comparison, IList formats) + { + if (!condition || formats.Count == 0) return queryable; + + switch (comparison) + { + case FilterComparison.Equal: + case FilterComparison.Contains: + return queryable.Where(s => formats.Contains(s.Format)); + case FilterComparison.NotContains: + case FilterComparison.NotEqual: + return queryable.Where(s => !formats.Contains(s.Format)); + case FilterComparison.MustContains: + case FilterComparison.GreaterThan: + case FilterComparison.GreaterThanEqual: + case FilterComparison.LessThan: + case FilterComparison.LessThanEqual: + case FilterComparison.Matches: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.IsEmpty: + throw new KavitaException($"{comparison} not applicable for Series.Format"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + } + + public IQueryable HasCollectionTags(bool condition, + FilterComparison comparison, IList collectionTags, IList collectionSeries) + { + if (!condition || (comparison != FilterComparison.IsEmpty && collectionTags.Count == 0)) return queryable; + + + switch (comparison) + { + case FilterComparison.Equal: + case FilterComparison.Contains: + return queryable.Where(s => collectionSeries.Contains(s.Id)); + case FilterComparison.NotContains: + case FilterComparison.NotEqual: + return queryable.Where(s => !collectionSeries.Contains(s.Id)); + case FilterComparison.MustContains: + // // Deconstruct and do a Union of a bunch of where statements since this doesn't translate + var queries = new List>() + { + queryable + }; + queries.AddRange(collectionSeries.Select(gId => queryable.Where(s => collectionSeries.Any(p => p == s.Id)))); + + return queries.Aggregate((q1, q2) => q1.Intersect(q2)); + case FilterComparison.IsEmpty: + return queryable.Where(s => s.Collections.Count == 0); + case FilterComparison.GreaterThan: + case FilterComparison.GreaterThanEqual: + case FilterComparison.LessThan: + case FilterComparison.LessThanEqual: + case FilterComparison.Matches: + case FilterComparison.BeginsWith: + case FilterComparison.EndsWith: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + throw new KavitaException($"{comparison} not applicable for Series.CollectionTags"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); + } + } + + public IQueryable HasName(bool condition, + FilterComparison comparison, string queryString) + { + if (string.IsNullOrEmpty(queryString) || !condition) return queryable; + + switch (comparison) + { + case FilterComparison.Equal: + return queryable.Where(s => s.Name.Equals(queryString) + || s.OriginalName.Equals(queryString) + || s.LocalizedName.Equals(queryString) + || s.SortName.Equals(queryString)); + case FilterComparison.BeginsWith: + return queryable.Where(s => EF.Functions.Like(s.Name, $"{queryString}%") + ||EF.Functions.Like(s.OriginalName, $"{queryString}%") + || EF.Functions.Like(s.LocalizedName, $"{queryString}%") + || EF.Functions.Like(s.SortName, $"{queryString}%")); + case FilterComparison.EndsWith: + return queryable.Where(s => EF.Functions.Like(s.Name, $"%{queryString}") + ||EF.Functions.Like(s.OriginalName, $"%{queryString}") + || EF.Functions.Like(s.LocalizedName, $"%{queryString}") + || EF.Functions.Like(s.SortName, $"%{queryString}")); + case FilterComparison.Matches: + return queryable.Where(s => EF.Functions.Like(s.Name, $"%{queryString}%") + ||EF.Functions.Like(s.OriginalName, $"%{queryString}%") + || EF.Functions.Like(s.LocalizedName, $"%{queryString}%") + || EF.Functions.Like(s.SortName, $"%{queryString}%")); + case FilterComparison.NotEqual: + return queryable.Where(s => s.Name != queryString + || s.OriginalName != queryString + || s.LocalizedName != queryString + || s.SortName != queryString); + case FilterComparison.NotContains: + case FilterComparison.GreaterThan: + case FilterComparison.GreaterThanEqual: + case FilterComparison.LessThan: + case FilterComparison.LessThanEqual: + case FilterComparison.Contains: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: + case FilterComparison.IsEmpty: + throw new KavitaException($"{comparison} not applicable for Series.Name"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"); + } + } + + public IQueryable HasSummary(bool condition, + FilterComparison comparison, string queryString) + { + if (!condition) return queryable; + + switch (comparison) + { + case FilterComparison.Equal: + return queryable.Where(s => s.Metadata.Summary.Equals(queryString)); + case FilterComparison.BeginsWith: + return queryable.Where(s => EF.Functions.Like(s.Metadata.Summary, $"{queryString}%")); + case FilterComparison.EndsWith: + return queryable.Where(s => EF.Functions.Like(s.Metadata.Summary, $"%{queryString}")); + case FilterComparison.Matches: + return queryable.Where(s => EF.Functions.Like(s.Metadata.Summary, $"%{queryString}%")); + case FilterComparison.NotEqual: + return queryable.Where(s => s.Metadata.Summary != queryString); + case FilterComparison.IsEmpty: + return queryable.Where(s => string.IsNullOrEmpty(s.Metadata.Summary)); + case FilterComparison.NotContains: + case FilterComparison.GreaterThan: + case FilterComparison.GreaterThanEqual: + case FilterComparison.LessThan: + case FilterComparison.LessThanEqual: + case FilterComparison.Contains: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: + throw new KavitaException($"{comparison} not applicable for Series.Metadata.Summary"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"); + } + } + + public IQueryable HasPath(bool condition, + FilterComparison comparison, string queryString) + { + if (!condition) return queryable; + + var normalizedPath = queryString.NormalizePath(); + + switch (comparison) + { + case FilterComparison.Equal: + return queryable.Where(s => s.FolderPath != null && s.FolderPath.Equals(normalizedPath)); + case FilterComparison.BeginsWith: + return queryable.Where(s => s.FolderPath != null && EF.Functions.Like(s.FolderPath, $"{normalizedPath}%")); + case FilterComparison.EndsWith: + return queryable.Where(s => s.FolderPath != null && EF.Functions.Like(s.FolderPath, $"%{normalizedPath}")); + case FilterComparison.Matches: + return queryable.Where(s => s.FolderPath != null && EF.Functions.Like(s.FolderPath, $"%{normalizedPath}%")); + case FilterComparison.NotEqual: + return queryable.Where(s => s.FolderPath != null && s.FolderPath != normalizedPath); + case FilterComparison.NotContains: + case FilterComparison.GreaterThan: + case FilterComparison.GreaterThanEqual: + case FilterComparison.LessThan: + case FilterComparison.LessThanEqual: + case FilterComparison.Contains: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: + case FilterComparison.IsEmpty: + throw new KavitaException($"{comparison} not applicable for Series.FolderPath"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"); + } + } + + public IQueryable HasFilePath(bool condition, + FilterComparison comparison, string queryString) + { + if (!condition) return queryable; + + var normalizedPath = queryString.NormalizePath(); + + switch (comparison) + { + case FilterComparison.Equal: + return queryable.Where(s => + s.Volumes.Any(v => + v.Chapters.Any(c => + c.Files.Any(f => + f.FilePath != null && f.FilePath.Equals(normalizedPath) + ) + ) + ) + ); + case FilterComparison.BeginsWith: + return queryable.Where(s => + s.Volumes.Any(v => + v.Chapters.Any(c => + c.Files.Any(f => + f.FilePath != null && EF.Functions.Like(f.FilePath, $"{normalizedPath}%") + ) + ) + ) + ); + case FilterComparison.EndsWith: + return queryable.Where(s => + s.Volumes.Any(v => + v.Chapters.Any(c => + c.Files.Any(f => + f.FilePath != null && EF.Functions.Like(f.FilePath, $"%{normalizedPath}") + ) + ) + ) + ); + case FilterComparison.Matches: + return queryable.Where(s => + s.Volumes.Any(v => + v.Chapters.Any(c => + c.Files.Any(f => + f.FilePath != null && EF.Functions.Like(f.FilePath, $"%{normalizedPath}%") + ) + ) + ) + ); + case FilterComparison.NotEqual: + return queryable.Where(s => + s.Volumes.Any(v => + v.Chapters.Any(c => + c.Files.Any(f => + f.FilePath == null || !f.FilePath.Equals(normalizedPath) + ) + ) + ) + ); + case FilterComparison.NotContains: + case FilterComparison.GreaterThan: + case FilterComparison.GreaterThanEqual: + case FilterComparison.LessThan: + case FilterComparison.LessThanEqual: + case FilterComparison.Contains: + case FilterComparison.IsBefore: + case FilterComparison.IsAfter: + case FilterComparison.IsInLast: + case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: + case FilterComparison.IsEmpty: + throw new KavitaException($"{comparison} not applicable for Series.FolderPath"); + default: + throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"); + } + } + + public IQueryable HasFileSize(bool condition, + FilterComparison comparison, float fileSize) + { + if (fileSize == 0f || !condition) return queryable; + + return comparison switch + { + FilterComparison.Equal => queryable.Where(s => s.Volumes.Sum(v => v.Chapters.Sum(c => c.Files.Sum(f => f.Bytes))) == fileSize), + FilterComparison.LessThan => queryable.Where(s => s.Volumes.Sum(v => v.Chapters.Sum(c => c.Files.Sum(f => f.Bytes))) < fileSize), + FilterComparison.LessThanEqual => queryable.Where(s => s.Volumes.Sum(v => v.Chapters.Sum(c => c.Files.Sum(f => f.Bytes))) <= fileSize), + FilterComparison.GreaterThan => queryable.Where(s => s.Volumes.Sum(v => v.Chapters.Sum(c => c.Files.Sum(f => f.Bytes))) > fileSize), + FilterComparison.GreaterThanEqual => queryable.Where(s => s.Volumes.Sum(v => v.Chapters.Sum(c => c.Files.Sum(f => f.Bytes))) >= fileSize), + _ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"), + }; + } + } +} diff --git a/API/Extensions/QueryExtensions/IncludesExtensions.cs b/Kavita.Database/Extensions/IncludesExtensions.cs similarity index 98% rename from API/Extensions/QueryExtensions/IncludesExtensions.cs rename to Kavita.Database/Extensions/IncludesExtensions.cs index 113dfb8f4..c0354d949 100644 --- a/API/Extensions/QueryExtensions/IncludesExtensions.cs +++ b/Kavita.Database/Extensions/IncludesExtensions.cs @@ -1,11 +1,11 @@ -using System.Linq; -using API.Data.Repositories; -using API.Entities; -using API.Entities.Person; +using System.Linq; +using Kavita.API.Repositories; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Person; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; -namespace API.Extensions.QueryExtensions; -#nullable enable +namespace Kavita.Database.Extensions; /// /// All extensions against IQueryable that enables the dynamic including based on bitwise flag pattern diff --git a/Kavita.Database/Extensions/PagedListExtensions.cs b/Kavita.Database/Extensions/PagedListExtensions.cs new file mode 100644 index 000000000..fd44d6623 --- /dev/null +++ b/Kavita.Database/Extensions/PagedListExtensions.cs @@ -0,0 +1,29 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Kavita.Common.Helpers; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Extensions; + +public static class PagedListExtensions +{ + extension(PagedList pagedList) + { + public static async Task> CreateAsync(IQueryable source, UserParams userParams, CancellationToken ct = default) + { + return await PagedList.CreateAsync(source, userParams.PageNumber, userParams.PageSize, ct); + } + + public static async Task> CreateAsync(IQueryable source, int pageNumber, int pageSize, CancellationToken ct = default) + { + // NOTE: OrderBy warning being thrown here even if query has the orderby statement + var countTask = source.CountAsync(ct); + var itemsTask = source.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToListAsync(ct); + + await Task.WhenAll(countTask, itemsTask); + + return PagedList.Create(itemsTask.Result, countTask.Result, pageNumber, pageSize); + } + } +} diff --git a/Kavita.Database/Extensions/ProjectToExtensions.cs b/Kavita.Database/Extensions/ProjectToExtensions.cs new file mode 100644 index 000000000..18769b7f8 --- /dev/null +++ b/Kavita.Database/Extensions/ProjectToExtensions.cs @@ -0,0 +1,25 @@ +using System.Linq; +using AutoMapper; +using AutoMapper.QueryableExtensions; + +namespace Kavita.Database.Extensions; + +public static class ProjectToExtensions +{ + extension(IQueryable queryable) + { + public IQueryable ProjectToWithProgress(IConfigurationProvider config, + int userId) + { + return queryable.ProjectTo(config, new { userId }); + } + + // Convenience overload taking IMapper directly + public IQueryable ProjectToWithProgress(IMapper mapper, + int userId) + { + return queryable.ProjectTo(mapper.ConfigurationProvider, new { userId }); + } + } +} + diff --git a/API/Extensions/QueryExtensions/QueryableExtensions.cs b/Kavita.Database/Extensions/QueryableExtensions.cs similarity index 97% rename from API/Extensions/QueryExtensions/QueryableExtensions.cs rename to Kavita.Database/Extensions/QueryableExtensions.cs index d433b6831..36fdc01de 100644 --- a/API/Extensions/QueryExtensions/QueryableExtensions.cs +++ b/Kavita.Database/Extensions/QueryableExtensions.cs @@ -3,19 +3,18 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; -using API.Data.Misc; -using API.Data.Repositories; -using API.DTOs.Annotations; -using API.DTOs.Filtering; -using API.DTOs.KavitaPlus.Manage; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Person; -using API.Entities.Scrobble; +using Kavita.API.Repositories; +using Kavita.Models.DTOs.Annotations; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.KavitaPlus.Manage; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Person; +using Kavita.Models.Entities.Scrobble; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; -namespace API.Extensions.QueryExtensions; -#nullable enable +namespace Kavita.Database.Extensions; public static class QueryableExtensions { diff --git a/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs b/Kavita.Database/Extensions/RestrictByAgeExtensions.cs similarity index 75% rename from API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs rename to Kavita.Database/Extensions/RestrictByAgeExtensions.cs index 038899a40..0951c2325 100644 --- a/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs +++ b/Kavita.Database/Extensions/RestrictByAgeExtensions.cs @@ -1,13 +1,11 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using API.Data.Misc; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Person; -using API.Entities.User; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Person; +using Kavita.Models.Entities.User; -namespace API.Extensions.QueryExtensions; -#nullable enable +namespace Kavita.Database.Extensions; /// /// Responsible for restricting Entities based on an AgeRestriction @@ -83,7 +81,7 @@ public static class RestrictByAgeExtensions } /// - /// Returns all Genres where any of the linked Series/Chapters are less than or equal to restriction age rating + /// Returns all Genres where any of the linked Series/Chapters are less than or equal to the restriction age rating /// /// /// @@ -178,80 +176,81 @@ public static class RestrictByAgeExtensions return q; } - private static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction, int userId) + /// + extension(IQueryable queryable) { - if (restriction.AgeRating == AgeRating.NotApplicable) return queryable; - var q = queryable.Where(a => a.Series.Metadata.AgeRating <= restriction.AgeRating || a.AppUserId == userId); - - if (!restriction.IncludeUnknowns) + private IQueryable RestrictAgainstAgeRestriction(AgeRestriction restriction, int userId) { - return q.Where(a => a.Series.Metadata.AgeRating != AgeRating.Unknown || a.AppUserId == userId); + if (restriction.AgeRating == AgeRating.NotApplicable) return queryable; + var q = queryable.Where(a => a.Series.Metadata.AgeRating <= restriction.AgeRating || a.AppUserId == userId); + + if (!restriction.IncludeUnknowns) + { + return q.Where(a => a.Series.Metadata.AgeRating != AgeRating.Unknown || a.AppUserId == userId); + } + + return q; } - return q; - } - - // TODO: After updating to .net 10, leverage new Complex Data type queries to inline all db operations here - /// - /// Filter annotations by social preferences of users - /// - /// - /// - /// List of user preferences for every user on the server - /// - public static IQueryable RestrictBySocialPreferences(this IQueryable queryable, int userId, IList userPreferences) - { - var preferencesById = userPreferences.ToDictionary(p => p.AppUserId, p => p.SocialPreferences); - var socialPreferences = preferencesById[userId]; - - if (socialPreferences.ViewOtherAnnotations) + /// + /// Filter annotations by social preferences of users + /// + /// + /// List of user preferences for every user on the server + /// + public IQueryable RestrictBySocialPreferences(int userId, IList userPreferences) { - // We are unable to do dictionary lookups in Sqlite; This means we need to translate them to X IN Y. - var sharingUserIds = userPreferences - .Where(p => p.SocialPreferences.ShareAnnotations) - .Select(p => p.AppUserId) - .ToHashSet(); + var preferencesById = userPreferences.ToDictionary(p => p.AppUserId, p => p.SocialPreferences); + var socialPreferences = preferencesById[userId]; - // Only include the users' annotations, or those of users that are sharing - queryable = queryable.Where(a => a.AppUserId == userId || sharingUserIds.Contains(a.AppUserId)); - - // For other users' annotation - foreach (var sharingUserId in sharingUserIds.Where(id => id != userId)) + if (socialPreferences.ViewOtherAnnotations) { - // Filter out libs if enabled - var libs = preferencesById[sharingUserId].SocialLibraries; - if (libs.Count > 0) - { - queryable = queryable.Where(a => a.AppUserId != sharingUserId || libs.Contains(a.LibraryId)); - } + // We are unable to do dictionary lookups in Sqlite; This means we need to translate them to X IN Y. + var sharingUserIds = userPreferences + .Where(p => p.SocialPreferences.ShareAnnotations) + .Select(p => p.AppUserId) + .ToHashSet(); - // Filter on age rating - var ageRating = preferencesById[sharingUserId].SocialMaxAgeRating; - var includeUnknowns = preferencesById[sharingUserId].SocialIncludeUnknowns; - if (ageRating != AgeRating.NotApplicable) + // Only include the users' annotations or those of users that are sharing + queryable = queryable.Where(a => a.AppUserId == userId || sharingUserIds.Contains(a.AppUserId)); + + // For other users' annotation + foreach (var sharingUserId in sharingUserIds.Where(id => id != userId)) { - queryable = queryable.Where(a => a.AppUserId != sharingUserId || a.Series.Metadata.AgeRating <= ageRating) - .WhereIf(!includeUnknowns, - a => a.AppUserId != sharingUserId || a.Series.Metadata.AgeRating != AgeRating.Unknown); + // Filter out libs if enabled + var libs = preferencesById[sharingUserId].SocialLibraries; + if (libs.Count > 0) + { + queryable = queryable.Where(a => a.AppUserId != sharingUserId || libs.Contains(a.LibraryId)); + } + + // Filter on age rating + var ageRating = preferencesById[sharingUserId].SocialMaxAgeRating; + var includeUnknowns = preferencesById[sharingUserId].SocialIncludeUnknowns; + if (ageRating != AgeRating.NotApplicable) + { + queryable = queryable.Where(a => a.AppUserId != sharingUserId || a.Series.Metadata.AgeRating <= ageRating) + .WhereIf(!includeUnknowns, + a => a.AppUserId != sharingUserId || a.Series.Metadata.AgeRating != AgeRating.Unknown); + } } } - } - else - { - queryable = queryable.Where(a => a.AppUserId == userId); - } - - return queryable - .WhereIf(socialPreferences.SocialLibraries.Count > 0, - a => a.AppUserId == userId || socialPreferences.SocialLibraries.Contains(a.LibraryId)) - .RestrictAgainstAgeRestriction(new AgeRestriction + else { - AgeRating = socialPreferences.SocialMaxAgeRating, - IncludeUnknowns = socialPreferences.SocialIncludeUnknowns, - }, userId); + queryable = queryable.Where(a => a.AppUserId == userId); + } + + return queryable + .WhereIf(socialPreferences.SocialLibraries.Count > 0, + a => a.AppUserId == userId || socialPreferences.SocialLibraries.Contains(a.LibraryId)) + .RestrictAgainstAgeRestriction(new AgeRestriction + { + AgeRating = socialPreferences.SocialMaxAgeRating, + IncludeUnknowns = socialPreferences.SocialIncludeUnknowns, + }, userId); + } } - // TODO: After updating to .net 10, leverage new Complex Data type queries to inline all db operations here /// /// Filter user reviews social preferences of users /// @@ -299,7 +298,6 @@ public static class RestrictByAgeExtensions }, userId); } - // TODO: After updating to .net 10, leverage new Complex Data type queries to inline all db operations here /// /// Filter user chapter reviews social preferences of users /// diff --git a/API/Extensions/QueryExtensions/RestrictByLibraryExtensions.cs b/Kavita.Database/Extensions/RestrictByLibraryExtensions.cs similarity index 89% rename from API/Extensions/QueryExtensions/RestrictByLibraryExtensions.cs rename to Kavita.Database/Extensions/RestrictByLibraryExtensions.cs index 9ec1b8621..bece6474a 100644 --- a/API/Extensions/QueryExtensions/RestrictByLibraryExtensions.cs +++ b/Kavita.Database/Extensions/RestrictByLibraryExtensions.cs @@ -1,12 +1,11 @@ -using System.Linq; -using API.Entities; -using API.Entities.Person; +using System.Linq; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Person; -namespace API.Extensions.QueryExtensions; +namespace Kavita.Database.Extensions; public static class RestrictByLibraryExtensions { - public static IQueryable RestrictByLibrary(this IQueryable query, IQueryable userLibs) { return query.Where(p => diff --git a/API/Extensions/QueryExtensions/Filtering/SearchQueryableExtensions.cs b/Kavita.Database/Extensions/SearchQueryableExtensions.cs similarity index 93% rename from API/Extensions/QueryExtensions/Filtering/SearchQueryableExtensions.cs rename to Kavita.Database/Extensions/SearchQueryableExtensions.cs index 173e3dedc..e8aff4e32 100644 --- a/API/Extensions/QueryExtensions/Filtering/SearchQueryableExtensions.cs +++ b/Kavita.Database/Extensions/SearchQueryableExtensions.cs @@ -1,13 +1,13 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using API.Data.Misc; -using API.Data.Repositories; -using API.Entities; -using API.Entities.Metadata; -using API.Entities.Person; +using Kavita.API.Repositories; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.Person; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; -namespace API.Extensions.QueryExtensions.Filtering; +namespace Kavita.Database.Extensions; public static class SearchQueryableExtensions { diff --git a/API/Extensions/QueryExtensions/Filtering/SeriesSort.cs b/Kavita.Database/Extensions/SeriesSortExtensions.cs similarity index 91% rename from API/Extensions/QueryExtensions/Filtering/SeriesSort.cs rename to Kavita.Database/Extensions/SeriesSortExtensions.cs index 8f0e9a364..7f798e958 100644 --- a/API/Extensions/QueryExtensions/Filtering/SeriesSort.cs +++ b/Kavita.Database/Extensions/SeriesSortExtensions.cs @@ -1,17 +1,17 @@ using System.Linq; -using API.DTOs.Filtering; -using API.Entities; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.Entities; using Microsoft.EntityFrameworkCore; -namespace API.Extensions.QueryExtensions.Filtering; -#nullable enable +namespace Kavita.Database.Extensions; -public static class SeriesSort +public static class SeriesSortExtensions { /// /// Applies the correct sort based on /// /// + /// /// /// public static IQueryable Sort(this IQueryable query, int userId, SortOptions? sortOptions) diff --git a/API/Extensions/QueryExtensions/StatisticsQueryExtensions.cs b/Kavita.Database/Extensions/StatisticsQueryExtensions.cs similarity index 74% rename from API/Extensions/QueryExtensions/StatisticsQueryExtensions.cs rename to Kavita.Database/Extensions/StatisticsQueryExtensions.cs index 11a2b7bdf..93da861d1 100644 --- a/API/Extensions/QueryExtensions/StatisticsQueryExtensions.cs +++ b/Kavita.Database/Extensions/StatisticsQueryExtensions.cs @@ -1,12 +1,12 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; -using API.Entities.Progress; +using Kavita.Models.Entities.Progress; using Microsoft.EntityFrameworkCore; -namespace API.Extensions.QueryExtensions; +namespace Kavita.Database.Extensions; public class IdCount { @@ -19,7 +19,7 @@ public class IdCount /// public static class StatisticsQueryExtensions { - public static async Task> GetTopCounts( this IQueryable query, Expression> keySelector, int? take = null) + public static async Task> GetTopCounts(this IQueryable query, Expression> keySelector, int? take = null) { var result = query .GroupBy(keySelector) diff --git a/Kavita.Database/Kavita.Database.csproj b/Kavita.Database/Kavita.Database.csproj new file mode 100644 index 000000000..c621ee196 --- /dev/null +++ b/Kavita.Database/Kavita.Database.csproj @@ -0,0 +1,24 @@ + + + + net10.0 + disable + enable + + + + + + + + + + + + + + + + + + diff --git a/API/Data/Migrations/20201213205325_AddUser.Designer.cs b/Kavita.Database/Migrations/20201213205325_AddUser.Designer.cs similarity index 96% rename from API/Data/Migrations/20201213205325_AddUser.Designer.cs rename to Kavita.Database/Migrations/20201213205325_AddUser.Designer.cs index 565d03517..8e72096fb 100644 --- a/API/Data/Migrations/20201213205325_AddUser.Designer.cs +++ b/Kavita.Database/Migrations/20201213205325_AddUser.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20201213205325_AddUser")] diff --git a/API/Data/Migrations/20201213205325_AddUser.cs b/Kavita.Database/Migrations/20201213205325_AddUser.cs similarity index 97% rename from API/Data/Migrations/20201213205325_AddUser.cs rename to Kavita.Database/Migrations/20201213205325_AddUser.cs index 4429111b1..57322fdbb 100644 --- a/API/Data/Migrations/20201213205325_AddUser.cs +++ b/Kavita.Database/Migrations/20201213205325_AddUser.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class AddUser : Migration { diff --git a/API/Data/Migrations/20201215195007_AddedLibrary.Designer.cs b/Kavita.Database/Migrations/20201215195007_AddedLibrary.Designer.cs similarity index 98% rename from API/Data/Migrations/20201215195007_AddedLibrary.Designer.cs rename to Kavita.Database/Migrations/20201215195007_AddedLibrary.Designer.cs index 4a657771e..fc14610f7 100644 --- a/API/Data/Migrations/20201215195007_AddedLibrary.Designer.cs +++ b/Kavita.Database/Migrations/20201215195007_AddedLibrary.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20201215195007_AddedLibrary")] diff --git a/API/Data/Migrations/20201215195007_AddedLibrary.cs b/Kavita.Database/Migrations/20201215195007_AddedLibrary.cs similarity index 98% rename from API/Data/Migrations/20201215195007_AddedLibrary.cs rename to Kavita.Database/Migrations/20201215195007_AddedLibrary.cs index f1c4adf56..1c0a688be 100644 --- a/API/Data/Migrations/20201215195007_AddedLibrary.cs +++ b/Kavita.Database/Migrations/20201215195007_AddedLibrary.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class AddedLibrary : Migration { diff --git a/API/Data/Migrations/20201218173135_ManyToManyLibraries.Designer.cs b/Kavita.Database/Migrations/20201218173135_ManyToManyLibraries.Designer.cs similarity index 98% rename from API/Data/Migrations/20201218173135_ManyToManyLibraries.Designer.cs rename to Kavita.Database/Migrations/20201218173135_ManyToManyLibraries.Designer.cs index 98af0c730..4a1b042b6 100644 --- a/API/Data/Migrations/20201218173135_ManyToManyLibraries.Designer.cs +++ b/Kavita.Database/Migrations/20201218173135_ManyToManyLibraries.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20201218173135_ManyToManyLibraries")] diff --git a/API/Data/Migrations/20201218173135_ManyToManyLibraries.cs b/Kavita.Database/Migrations/20201218173135_ManyToManyLibraries.cs similarity index 99% rename from API/Data/Migrations/20201218173135_ManyToManyLibraries.cs rename to Kavita.Database/Migrations/20201218173135_ManyToManyLibraries.cs index e7d2cb39b..4a7586add 100644 --- a/API/Data/Migrations/20201218173135_ManyToManyLibraries.cs +++ b/Kavita.Database/Migrations/20201218173135_ManyToManyLibraries.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ManyToManyLibraries : Migration { diff --git a/API/Data/Migrations/20201221141047_IdentityAdded.Designer.cs b/Kavita.Database/Migrations/20201221141047_IdentityAdded.Designer.cs similarity index 99% rename from API/Data/Migrations/20201221141047_IdentityAdded.Designer.cs rename to Kavita.Database/Migrations/20201221141047_IdentityAdded.Designer.cs index 0836f6f4a..b42232860 100644 --- a/API/Data/Migrations/20201221141047_IdentityAdded.Designer.cs +++ b/Kavita.Database/Migrations/20201221141047_IdentityAdded.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20201221141047_IdentityAdded")] diff --git a/API/Data/Migrations/20201221141047_IdentityAdded.cs b/Kavita.Database/Migrations/20201221141047_IdentityAdded.cs similarity index 99% rename from API/Data/Migrations/20201221141047_IdentityAdded.cs rename to Kavita.Database/Migrations/20201221141047_IdentityAdded.cs index ee9dd15b2..7e77010b6 100644 --- a/API/Data/Migrations/20201221141047_IdentityAdded.cs +++ b/Kavita.Database/Migrations/20201221141047_IdentityAdded.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class IdentityAdded : Migration { diff --git a/API/Data/Migrations/20201224155621_MiscCleanup.Designer.cs b/Kavita.Database/Migrations/20201224155621_MiscCleanup.Designer.cs similarity index 99% rename from API/Data/Migrations/20201224155621_MiscCleanup.Designer.cs rename to Kavita.Database/Migrations/20201224155621_MiscCleanup.Designer.cs index 8ae8c597a..38894dbeb 100644 --- a/API/Data/Migrations/20201224155621_MiscCleanup.Designer.cs +++ b/Kavita.Database/Migrations/20201224155621_MiscCleanup.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20201224155621_MiscCleanup")] diff --git a/API/Data/Migrations/20201224155621_MiscCleanup.cs b/Kavita.Database/Migrations/20201224155621_MiscCleanup.cs similarity index 97% rename from API/Data/Migrations/20201224155621_MiscCleanup.cs rename to Kavita.Database/Migrations/20201224155621_MiscCleanup.cs index 78e66aea8..f2ead54d9 100644 --- a/API/Data/Migrations/20201224155621_MiscCleanup.cs +++ b/Kavita.Database/Migrations/20201224155621_MiscCleanup.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class MiscCleanup : Migration { diff --git a/API/Data/Migrations/20201229190216_SeriesAndVolumeEntities.Designer.cs b/Kavita.Database/Migrations/20201229190216_SeriesAndVolumeEntities.Designer.cs similarity index 99% rename from API/Data/Migrations/20201229190216_SeriesAndVolumeEntities.Designer.cs rename to Kavita.Database/Migrations/20201229190216_SeriesAndVolumeEntities.Designer.cs index 5cf25a225..5bb9643a9 100644 --- a/API/Data/Migrations/20201229190216_SeriesAndVolumeEntities.Designer.cs +++ b/Kavita.Database/Migrations/20201229190216_SeriesAndVolumeEntities.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20201229190216_SeriesAndVolumeEntities")] diff --git a/API/Data/Migrations/20201229190216_SeriesAndVolumeEntities.cs b/Kavita.Database/Migrations/20201229190216_SeriesAndVolumeEntities.cs similarity index 99% rename from API/Data/Migrations/20201229190216_SeriesAndVolumeEntities.cs rename to Kavita.Database/Migrations/20201229190216_SeriesAndVolumeEntities.cs index 5b4302ba3..bfa689b92 100644 --- a/API/Data/Migrations/20201229190216_SeriesAndVolumeEntities.cs +++ b/Kavita.Database/Migrations/20201229190216_SeriesAndVolumeEntities.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SeriesAndVolumeEntities : Migration { diff --git a/API/Data/Migrations/20210101180935_AddedCoverImageToSeries.Designer.cs b/Kavita.Database/Migrations/20210101180935_AddedCoverImageToSeries.Designer.cs similarity index 99% rename from API/Data/Migrations/20210101180935_AddedCoverImageToSeries.Designer.cs rename to Kavita.Database/Migrations/20210101180935_AddedCoverImageToSeries.Designer.cs index a1a54360f..57d044b29 100644 --- a/API/Data/Migrations/20210101180935_AddedCoverImageToSeries.Designer.cs +++ b/Kavita.Database/Migrations/20210101180935_AddedCoverImageToSeries.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210101180935_AddedCoverImageToSeries")] diff --git a/API/Data/Migrations/20210101180935_AddedCoverImageToSeries.cs b/Kavita.Database/Migrations/20210101180935_AddedCoverImageToSeries.cs similarity index 94% rename from API/Data/Migrations/20210101180935_AddedCoverImageToSeries.cs rename to Kavita.Database/Migrations/20210101180935_AddedCoverImageToSeries.cs index 45e0fdc41..690015930 100644 --- a/API/Data/Migrations/20210101180935_AddedCoverImageToSeries.cs +++ b/Kavita.Database/Migrations/20210101180935_AddedCoverImageToSeries.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class AddedCoverImageToSeries : Migration { diff --git a/API/Data/Migrations/20210102165536_EntityTimestamps.Designer.cs b/Kavita.Database/Migrations/20210102165536_EntityTimestamps.Designer.cs similarity index 99% rename from API/Data/Migrations/20210102165536_EntityTimestamps.Designer.cs rename to Kavita.Database/Migrations/20210102165536_EntityTimestamps.Designer.cs index de4910b51..2cd579f6c 100644 --- a/API/Data/Migrations/20210102165536_EntityTimestamps.Designer.cs +++ b/Kavita.Database/Migrations/20210102165536_EntityTimestamps.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210102165536_EntityTimestamps")] diff --git a/API/Data/Migrations/20210102165536_EntityTimestamps.cs b/Kavita.Database/Migrations/20210102165536_EntityTimestamps.cs similarity index 98% rename from API/Data/Migrations/20210102165536_EntityTimestamps.cs rename to Kavita.Database/Migrations/20210102165536_EntityTimestamps.cs index 2ed6041f0..4fa0ef673 100644 --- a/API/Data/Migrations/20210102165536_EntityTimestamps.cs +++ b/Kavita.Database/Migrations/20210102165536_EntityTimestamps.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class EntityTimestamps : Migration { diff --git a/API/Data/Migrations/20210102173326_VolumeNumberRefactor.Designer.cs b/Kavita.Database/Migrations/20210102173326_VolumeNumberRefactor.Designer.cs similarity index 99% rename from API/Data/Migrations/20210102173326_VolumeNumberRefactor.Designer.cs rename to Kavita.Database/Migrations/20210102173326_VolumeNumberRefactor.Designer.cs index 1102111fc..9a20fe065 100644 --- a/API/Data/Migrations/20210102173326_VolumeNumberRefactor.Designer.cs +++ b/Kavita.Database/Migrations/20210102173326_VolumeNumberRefactor.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210102173326_VolumeNumberRefactor")] diff --git a/API/Data/Migrations/20210102173326_VolumeNumberRefactor.cs b/Kavita.Database/Migrations/20210102173326_VolumeNumberRefactor.cs similarity index 96% rename from API/Data/Migrations/20210102173326_VolumeNumberRefactor.cs rename to Kavita.Database/Migrations/20210102173326_VolumeNumberRefactor.cs index 21cc8d42c..24265650b 100644 --- a/API/Data/Migrations/20210102173326_VolumeNumberRefactor.cs +++ b/Kavita.Database/Migrations/20210102173326_VolumeNumberRefactor.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class VolumeNumberRefactor : Migration { diff --git a/API/Data/Migrations/20210103201043_RemoveUserIsAdmin.Designer.cs b/Kavita.Database/Migrations/20210103201043_RemoveUserIsAdmin.Designer.cs similarity index 99% rename from API/Data/Migrations/20210103201043_RemoveUserIsAdmin.Designer.cs rename to Kavita.Database/Migrations/20210103201043_RemoveUserIsAdmin.Designer.cs index 4288a9878..8e9c3ceba 100644 --- a/API/Data/Migrations/20210103201043_RemoveUserIsAdmin.Designer.cs +++ b/Kavita.Database/Migrations/20210103201043_RemoveUserIsAdmin.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210103201043_RemoveUserIsAdmin")] diff --git a/API/Data/Migrations/20210103201043_RemoveUserIsAdmin.cs b/Kavita.Database/Migrations/20210103201043_RemoveUserIsAdmin.cs similarity index 94% rename from API/Data/Migrations/20210103201043_RemoveUserIsAdmin.cs rename to Kavita.Database/Migrations/20210103201043_RemoveUserIsAdmin.cs index 826159fbb..a9e0e0fca 100644 --- a/API/Data/Migrations/20210103201043_RemoveUserIsAdmin.cs +++ b/Kavita.Database/Migrations/20210103201043_RemoveUserIsAdmin.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class RemoveUserIsAdmin : Migration { diff --git a/API/Data/Migrations/20210103230812_SeriesCoverImage.Designer.cs b/Kavita.Database/Migrations/20210103230812_SeriesCoverImage.Designer.cs similarity index 99% rename from API/Data/Migrations/20210103230812_SeriesCoverImage.Designer.cs rename to Kavita.Database/Migrations/20210103230812_SeriesCoverImage.Designer.cs index 03f94a6a2..3ad597825 100644 --- a/API/Data/Migrations/20210103230812_SeriesCoverImage.Designer.cs +++ b/Kavita.Database/Migrations/20210103230812_SeriesCoverImage.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210103230812_SeriesCoverImage")] diff --git a/API/Data/Migrations/20210103230812_SeriesCoverImage.cs b/Kavita.Database/Migrations/20210103230812_SeriesCoverImage.cs similarity index 96% rename from API/Data/Migrations/20210103230812_SeriesCoverImage.cs rename to Kavita.Database/Migrations/20210103230812_SeriesCoverImage.cs index 9436cbdcf..24c81a886 100644 --- a/API/Data/Migrations/20210103230812_SeriesCoverImage.cs +++ b/Kavita.Database/Migrations/20210103230812_SeriesCoverImage.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SeriesCoverImage : Migration { diff --git a/API/Data/Migrations/20210104011624_VolumeCoverImage.Designer.cs b/Kavita.Database/Migrations/20210104011624_VolumeCoverImage.Designer.cs similarity index 99% rename from API/Data/Migrations/20210104011624_VolumeCoverImage.Designer.cs rename to Kavita.Database/Migrations/20210104011624_VolumeCoverImage.Designer.cs index 437daca24..b479e98ba 100644 --- a/API/Data/Migrations/20210104011624_VolumeCoverImage.Designer.cs +++ b/Kavita.Database/Migrations/20210104011624_VolumeCoverImage.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210104011624_VolumeCoverImage")] diff --git a/API/Data/Migrations/20210104011624_VolumeCoverImage.cs b/Kavita.Database/Migrations/20210104011624_VolumeCoverImage.cs similarity index 94% rename from API/Data/Migrations/20210104011624_VolumeCoverImage.cs rename to Kavita.Database/Migrations/20210104011624_VolumeCoverImage.cs index 49bc17fea..108858368 100644 --- a/API/Data/Migrations/20210104011624_VolumeCoverImage.cs +++ b/Kavita.Database/Migrations/20210104011624_VolumeCoverImage.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class VolumeCoverImage : Migration { diff --git a/API/Data/Migrations/20210109205034_CacheMetadata.Designer.cs b/Kavita.Database/Migrations/20210109205034_CacheMetadata.Designer.cs similarity index 99% rename from API/Data/Migrations/20210109205034_CacheMetadata.Designer.cs rename to Kavita.Database/Migrations/20210109205034_CacheMetadata.Designer.cs index 66b17bf30..8acd11aaa 100644 --- a/API/Data/Migrations/20210109205034_CacheMetadata.Designer.cs +++ b/Kavita.Database/Migrations/20210109205034_CacheMetadata.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210109205034_CacheMetadata")] diff --git a/API/Data/Migrations/20210109205034_CacheMetadata.cs b/Kavita.Database/Migrations/20210109205034_CacheMetadata.cs similarity index 97% rename from API/Data/Migrations/20210109205034_CacheMetadata.cs rename to Kavita.Database/Migrations/20210109205034_CacheMetadata.cs index 476591e15..a12c93303 100644 --- a/API/Data/Migrations/20210109205034_CacheMetadata.cs +++ b/Kavita.Database/Migrations/20210109205034_CacheMetadata.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class CacheMetadata : Migration { diff --git a/API/Data/Migrations/20210111231840_VolumePages.Designer.cs b/Kavita.Database/Migrations/20210111231840_VolumePages.Designer.cs similarity index 99% rename from API/Data/Migrations/20210111231840_VolumePages.Designer.cs rename to Kavita.Database/Migrations/20210111231840_VolumePages.Designer.cs index f351a04e1..db1dc5f1c 100644 --- a/API/Data/Migrations/20210111231840_VolumePages.Designer.cs +++ b/Kavita.Database/Migrations/20210111231840_VolumePages.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210111231840_VolumePages")] diff --git a/API/Data/Migrations/20210111231840_VolumePages.cs b/Kavita.Database/Migrations/20210111231840_VolumePages.cs similarity index 94% rename from API/Data/Migrations/20210111231840_VolumePages.cs rename to Kavita.Database/Migrations/20210111231840_VolumePages.cs index c9b36b03a..b96b58ecc 100644 --- a/API/Data/Migrations/20210111231840_VolumePages.cs +++ b/Kavita.Database/Migrations/20210111231840_VolumePages.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class VolumePages : Migration { diff --git a/API/Data/Migrations/20210114214506_UserProgress.Designer.cs b/Kavita.Database/Migrations/20210114214506_UserProgress.Designer.cs similarity index 99% rename from API/Data/Migrations/20210114214506_UserProgress.Designer.cs rename to Kavita.Database/Migrations/20210114214506_UserProgress.Designer.cs index cd7e5a53b..b64bcbe83 100644 --- a/API/Data/Migrations/20210114214506_UserProgress.Designer.cs +++ b/Kavita.Database/Migrations/20210114214506_UserProgress.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210114214506_UserProgress")] diff --git a/API/Data/Migrations/20210114214506_UserProgress.cs b/Kavita.Database/Migrations/20210114214506_UserProgress.cs similarity index 98% rename from API/Data/Migrations/20210114214506_UserProgress.cs rename to Kavita.Database/Migrations/20210114214506_UserProgress.cs index 6d966fbdc..f22582bad 100644 --- a/API/Data/Migrations/20210114214506_UserProgress.cs +++ b/Kavita.Database/Migrations/20210114214506_UserProgress.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class UserProgress : Migration { diff --git a/API/Data/Migrations/20210117180406_ReadStatusModifications.Designer.cs b/Kavita.Database/Migrations/20210117180406_ReadStatusModifications.Designer.cs similarity index 99% rename from API/Data/Migrations/20210117180406_ReadStatusModifications.Designer.cs rename to Kavita.Database/Migrations/20210117180406_ReadStatusModifications.Designer.cs index d4133c335..36c45de23 100644 --- a/API/Data/Migrations/20210117180406_ReadStatusModifications.Designer.cs +++ b/Kavita.Database/Migrations/20210117180406_ReadStatusModifications.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210117180406_ReadStatusModifications")] diff --git a/API/Data/Migrations/20210117180406_ReadStatusModifications.cs b/Kavita.Database/Migrations/20210117180406_ReadStatusModifications.cs similarity index 99% rename from API/Data/Migrations/20210117180406_ReadStatusModifications.cs rename to Kavita.Database/Migrations/20210117180406_ReadStatusModifications.cs index d852d8843..d346cceb3 100644 --- a/API/Data/Migrations/20210117180406_ReadStatusModifications.cs +++ b/Kavita.Database/Migrations/20210117180406_ReadStatusModifications.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ReadStatusModifications : Migration { diff --git a/API/Data/Migrations/20210117181421_SeriesPages.Designer.cs b/Kavita.Database/Migrations/20210117181421_SeriesPages.Designer.cs similarity index 99% rename from API/Data/Migrations/20210117181421_SeriesPages.Designer.cs rename to Kavita.Database/Migrations/20210117181421_SeriesPages.Designer.cs index 8caa3acc1..b8f218fcd 100644 --- a/API/Data/Migrations/20210117181421_SeriesPages.Designer.cs +++ b/Kavita.Database/Migrations/20210117181421_SeriesPages.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210117181421_SeriesPages")] diff --git a/API/Data/Migrations/20210117181421_SeriesPages.cs b/Kavita.Database/Migrations/20210117181421_SeriesPages.cs similarity index 94% rename from API/Data/Migrations/20210117181421_SeriesPages.cs rename to Kavita.Database/Migrations/20210117181421_SeriesPages.cs index 97ee23b1b..43cbf2a93 100644 --- a/API/Data/Migrations/20210117181421_SeriesPages.cs +++ b/Kavita.Database/Migrations/20210117181421_SeriesPages.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SeriesPages : Migration { diff --git a/API/Data/Migrations/20210119213837_AppUserRatingAndReviews.Designer.cs b/Kavita.Database/Migrations/20210119213837_AppUserRatingAndReviews.Designer.cs similarity index 99% rename from API/Data/Migrations/20210119213837_AppUserRatingAndReviews.Designer.cs rename to Kavita.Database/Migrations/20210119213837_AppUserRatingAndReviews.Designer.cs index e68e9e11b..183cb172e 100644 --- a/API/Data/Migrations/20210119213837_AppUserRatingAndReviews.Designer.cs +++ b/Kavita.Database/Migrations/20210119213837_AppUserRatingAndReviews.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210119213837_AppUserRatingAndReviews")] diff --git a/API/Data/Migrations/20210119213837_AppUserRatingAndReviews.cs b/Kavita.Database/Migrations/20210119213837_AppUserRatingAndReviews.cs similarity index 97% rename from API/Data/Migrations/20210119213837_AppUserRatingAndReviews.cs rename to Kavita.Database/Migrations/20210119213837_AppUserRatingAndReviews.cs index 98db3af2b..52b4a01ac 100644 --- a/API/Data/Migrations/20210119213837_AppUserRatingAndReviews.cs +++ b/Kavita.Database/Migrations/20210119213837_AppUserRatingAndReviews.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class AppUserRatingAndReviews : Migration { diff --git a/API/Data/Migrations/20210121180051_AddedServerSettings.Designer.cs b/Kavita.Database/Migrations/20210121180051_AddedServerSettings.Designer.cs similarity index 99% rename from API/Data/Migrations/20210121180051_AddedServerSettings.Designer.cs rename to Kavita.Database/Migrations/20210121180051_AddedServerSettings.Designer.cs index 23894ae47..7b3959795 100644 --- a/API/Data/Migrations/20210121180051_AddedServerSettings.Designer.cs +++ b/Kavita.Database/Migrations/20210121180051_AddedServerSettings.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210121180051_AddedServerSettings")] diff --git a/API/Data/Migrations/20210121180051_AddedServerSettings.cs b/Kavita.Database/Migrations/20210121180051_AddedServerSettings.cs similarity index 96% rename from API/Data/Migrations/20210121180051_AddedServerSettings.cs rename to Kavita.Database/Migrations/20210121180051_AddedServerSettings.cs index 98fb77452..3cf0ca26d 100644 --- a/API/Data/Migrations/20210121180051_AddedServerSettings.cs +++ b/Kavita.Database/Migrations/20210121180051_AddedServerSettings.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class AddedServerSettings : Migration { diff --git a/API/Data/Migrations/20210121215532_ServerSettingsAdjustment.Designer.cs b/Kavita.Database/Migrations/20210121215532_ServerSettingsAdjustment.Designer.cs similarity index 99% rename from API/Data/Migrations/20210121215532_ServerSettingsAdjustment.Designer.cs rename to Kavita.Database/Migrations/20210121215532_ServerSettingsAdjustment.Designer.cs index 8072786e9..ccde420a1 100644 --- a/API/Data/Migrations/20210121215532_ServerSettingsAdjustment.Designer.cs +++ b/Kavita.Database/Migrations/20210121215532_ServerSettingsAdjustment.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210121215532_ServerSettingsAdjustment")] diff --git a/API/Data/Migrations/20210121215532_ServerSettingsAdjustment.cs b/Kavita.Database/Migrations/20210121215532_ServerSettingsAdjustment.cs similarity index 97% rename from API/Data/Migrations/20210121215532_ServerSettingsAdjustment.cs rename to Kavita.Database/Migrations/20210121215532_ServerSettingsAdjustment.cs index 6c1f1b268..5f340d3cd 100644 --- a/API/Data/Migrations/20210121215532_ServerSettingsAdjustment.cs +++ b/Kavita.Database/Migrations/20210121215532_ServerSettingsAdjustment.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ServerSettingsAdjustment : Migration { diff --git a/API/Data/Migrations/20210122165809_ServerSettingsChange.Designer.cs b/Kavita.Database/Migrations/20210122165809_ServerSettingsChange.Designer.cs similarity index 99% rename from API/Data/Migrations/20210122165809_ServerSettingsChange.Designer.cs rename to Kavita.Database/Migrations/20210122165809_ServerSettingsChange.Designer.cs index f277eae77..543811494 100644 --- a/API/Data/Migrations/20210122165809_ServerSettingsChange.Designer.cs +++ b/Kavita.Database/Migrations/20210122165809_ServerSettingsChange.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210122165809_ServerSettingsChange")] diff --git a/API/Data/Migrations/20210122165809_ServerSettingsChange.cs b/Kavita.Database/Migrations/20210122165809_ServerSettingsChange.cs similarity index 96% rename from API/Data/Migrations/20210122165809_ServerSettingsChange.cs rename to Kavita.Database/Migrations/20210122165809_ServerSettingsChange.cs index 69df81fa0..2aca691e0 100644 --- a/API/Data/Migrations/20210122165809_ServerSettingsChange.cs +++ b/Kavita.Database/Migrations/20210122165809_ServerSettingsChange.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ServerSettingsChange : Migration { diff --git a/API/Data/Migrations/20210122172455_ServerSettingsPrimaryKey.Designer.cs b/Kavita.Database/Migrations/20210122172455_ServerSettingsPrimaryKey.Designer.cs similarity index 99% rename from API/Data/Migrations/20210122172455_ServerSettingsPrimaryKey.Designer.cs rename to Kavita.Database/Migrations/20210122172455_ServerSettingsPrimaryKey.Designer.cs index 8bf49d1c5..59bf7173c 100644 --- a/API/Data/Migrations/20210122172455_ServerSettingsPrimaryKey.Designer.cs +++ b/Kavita.Database/Migrations/20210122172455_ServerSettingsPrimaryKey.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210122172455_ServerSettingsPrimaryKey")] diff --git a/API/Data/Migrations/20210122172455_ServerSettingsPrimaryKey.cs b/Kavita.Database/Migrations/20210122172455_ServerSettingsPrimaryKey.cs similarity index 98% rename from API/Data/Migrations/20210122172455_ServerSettingsPrimaryKey.cs rename to Kavita.Database/Migrations/20210122172455_ServerSettingsPrimaryKey.cs index 795c82683..efbe74035 100644 --- a/API/Data/Migrations/20210122172455_ServerSettingsPrimaryKey.cs +++ b/Kavita.Database/Migrations/20210122172455_ServerSettingsPrimaryKey.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ServerSettingsPrimaryKey : Migration { diff --git a/API/Data/Migrations/20210128143348_SeriesVolumeChapterChange.Designer.cs b/Kavita.Database/Migrations/20210128143348_SeriesVolumeChapterChange.Designer.cs similarity index 99% rename from API/Data/Migrations/20210128143348_SeriesVolumeChapterChange.Designer.cs rename to Kavita.Database/Migrations/20210128143348_SeriesVolumeChapterChange.Designer.cs index 17cb4b81d..1d248b40b 100644 --- a/API/Data/Migrations/20210128143348_SeriesVolumeChapterChange.Designer.cs +++ b/Kavita.Database/Migrations/20210128143348_SeriesVolumeChapterChange.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210128143348_SeriesVolumeChapterChange")] diff --git a/API/Data/Migrations/20210128143348_SeriesVolumeChapterChange.cs b/Kavita.Database/Migrations/20210128143348_SeriesVolumeChapterChange.cs similarity index 99% rename from API/Data/Migrations/20210128143348_SeriesVolumeChapterChange.cs rename to Kavita.Database/Migrations/20210128143348_SeriesVolumeChapterChange.cs index ae6e6b6d1..0c713d109 100644 --- a/API/Data/Migrations/20210128143348_SeriesVolumeChapterChange.cs +++ b/Kavita.Database/Migrations/20210128143348_SeriesVolumeChapterChange.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SeriesVolumeChapterChange : Migration { diff --git a/API/Data/Migrations/20210128201832_MangaFileChapterRelationship.Designer.cs b/Kavita.Database/Migrations/20210128201832_MangaFileChapterRelationship.Designer.cs similarity index 99% rename from API/Data/Migrations/20210128201832_MangaFileChapterRelationship.Designer.cs rename to Kavita.Database/Migrations/20210128201832_MangaFileChapterRelationship.Designer.cs index 5d0cfa7b5..3ef67df0a 100644 --- a/API/Data/Migrations/20210128201832_MangaFileChapterRelationship.Designer.cs +++ b/Kavita.Database/Migrations/20210128201832_MangaFileChapterRelationship.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210128201832_MangaFileChapterRelationship")] diff --git a/API/Data/Migrations/20210128201832_MangaFileChapterRelationship.cs b/Kavita.Database/Migrations/20210128201832_MangaFileChapterRelationship.cs similarity index 98% rename from API/Data/Migrations/20210128201832_MangaFileChapterRelationship.cs rename to Kavita.Database/Migrations/20210128201832_MangaFileChapterRelationship.cs index a04e77dd2..e120ffc39 100644 --- a/API/Data/Migrations/20210128201832_MangaFileChapterRelationship.cs +++ b/Kavita.Database/Migrations/20210128201832_MangaFileChapterRelationship.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class MangaFileChapterRelationship : Migration { diff --git a/API/Data/Migrations/20210203164258_ServerSettingsKey.Designer.cs b/Kavita.Database/Migrations/20210203164258_ServerSettingsKey.Designer.cs similarity index 99% rename from API/Data/Migrations/20210203164258_ServerSettingsKey.Designer.cs rename to Kavita.Database/Migrations/20210203164258_ServerSettingsKey.Designer.cs index 75d0a2244..3c949a520 100644 --- a/API/Data/Migrations/20210203164258_ServerSettingsKey.Designer.cs +++ b/Kavita.Database/Migrations/20210203164258_ServerSettingsKey.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210203164258_ServerSettingsKey")] diff --git a/API/Data/Migrations/20210203164258_ServerSettingsKey.cs b/Kavita.Database/Migrations/20210203164258_ServerSettingsKey.cs similarity index 95% rename from API/Data/Migrations/20210203164258_ServerSettingsKey.cs rename to Kavita.Database/Migrations/20210203164258_ServerSettingsKey.cs index 0a2a64920..13e51c1ec 100644 --- a/API/Data/Migrations/20210203164258_ServerSettingsKey.cs +++ b/Kavita.Database/Migrations/20210203164258_ServerSettingsKey.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ServerSettingsKey : Migration { diff --git a/API/Data/Migrations/20210205220227_UserPreferences.Designer.cs b/Kavita.Database/Migrations/20210205220227_UserPreferences.Designer.cs similarity index 99% rename from API/Data/Migrations/20210205220227_UserPreferences.Designer.cs rename to Kavita.Database/Migrations/20210205220227_UserPreferences.Designer.cs index 1bea7a402..bb47f4270 100644 --- a/API/Data/Migrations/20210205220227_UserPreferences.Designer.cs +++ b/Kavita.Database/Migrations/20210205220227_UserPreferences.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210205220227_UserPreferences")] diff --git a/API/Data/Migrations/20210205220227_UserPreferences.cs b/Kavita.Database/Migrations/20210205220227_UserPreferences.cs similarity index 98% rename from API/Data/Migrations/20210205220227_UserPreferences.cs rename to Kavita.Database/Migrations/20210205220227_UserPreferences.cs index 892eb9767..10f48c39a 100644 --- a/API/Data/Migrations/20210205220227_UserPreferences.cs +++ b/Kavita.Database/Migrations/20210205220227_UserPreferences.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class UserPreferences : Migration { diff --git a/API/Data/Migrations/20210207231256_SeriesNormalizedName.Designer.cs b/Kavita.Database/Migrations/20210207231256_SeriesNormalizedName.Designer.cs similarity index 99% rename from API/Data/Migrations/20210207231256_SeriesNormalizedName.Designer.cs rename to Kavita.Database/Migrations/20210207231256_SeriesNormalizedName.Designer.cs index 04c5c3d3d..86911ad4a 100644 --- a/API/Data/Migrations/20210207231256_SeriesNormalizedName.Designer.cs +++ b/Kavita.Database/Migrations/20210207231256_SeriesNormalizedName.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210207231256_SeriesNormalizedName")] diff --git a/API/Data/Migrations/20210207231256_SeriesNormalizedName.cs b/Kavita.Database/Migrations/20210207231256_SeriesNormalizedName.cs similarity index 94% rename from API/Data/Migrations/20210207231256_SeriesNormalizedName.cs rename to Kavita.Database/Migrations/20210207231256_SeriesNormalizedName.cs index 262583441..30422f129 100644 --- a/API/Data/Migrations/20210207231256_SeriesNormalizedName.cs +++ b/Kavita.Database/Migrations/20210207231256_SeriesNormalizedName.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SeriesNormalizedName : Migration { diff --git a/API/Data/Migrations/20210225150830_AddLocalizedName.Designer.cs b/Kavita.Database/Migrations/20210225150830_AddLocalizedName.Designer.cs similarity index 99% rename from API/Data/Migrations/20210225150830_AddLocalizedName.Designer.cs rename to Kavita.Database/Migrations/20210225150830_AddLocalizedName.Designer.cs index 04a9cc8de..1a5e22d04 100644 --- a/API/Data/Migrations/20210225150830_AddLocalizedName.Designer.cs +++ b/Kavita.Database/Migrations/20210225150830_AddLocalizedName.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210225150830_AddLocalizedName")] diff --git a/API/Data/Migrations/20210225150830_AddLocalizedName.cs b/Kavita.Database/Migrations/20210225150830_AddLocalizedName.cs similarity index 94% rename from API/Data/Migrations/20210225150830_AddLocalizedName.cs rename to Kavita.Database/Migrations/20210225150830_AddLocalizedName.cs index 4c8059dd6..b6de098a7 100644 --- a/API/Data/Migrations/20210225150830_AddLocalizedName.cs +++ b/Kavita.Database/Migrations/20210225150830_AddLocalizedName.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class AddLocalizedName : Migration { diff --git a/API/Data/Migrations/20210315134028_SearchIndexAndProgressDates.Designer.cs b/Kavita.Database/Migrations/20210315134028_SearchIndexAndProgressDates.Designer.cs similarity index 99% rename from API/Data/Migrations/20210315134028_SearchIndexAndProgressDates.Designer.cs rename to Kavita.Database/Migrations/20210315134028_SearchIndexAndProgressDates.Designer.cs index a407ccc28..8f05a7ec6 100644 --- a/API/Data/Migrations/20210315134028_SearchIndexAndProgressDates.Designer.cs +++ b/Kavita.Database/Migrations/20210315134028_SearchIndexAndProgressDates.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210315134028_SearchIndexAndProgressDates")] diff --git a/API/Data/Migrations/20210315134028_SearchIndexAndProgressDates.cs b/Kavita.Database/Migrations/20210315134028_SearchIndexAndProgressDates.cs similarity index 97% rename from API/Data/Migrations/20210315134028_SearchIndexAndProgressDates.cs rename to Kavita.Database/Migrations/20210315134028_SearchIndexAndProgressDates.cs index 02dc1db2c..9d0806398 100644 --- a/API/Data/Migrations/20210315134028_SearchIndexAndProgressDates.cs +++ b/Kavita.Database/Migrations/20210315134028_SearchIndexAndProgressDates.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SearchIndexAndProgressDates : Migration { diff --git a/API/Data/Migrations/20210322212724_MangaFileToPages.Designer.cs b/Kavita.Database/Migrations/20210322212724_MangaFileToPages.Designer.cs similarity index 99% rename from API/Data/Migrations/20210322212724_MangaFileToPages.Designer.cs rename to Kavita.Database/Migrations/20210322212724_MangaFileToPages.Designer.cs index f5d2d7ef9..2882f36f9 100644 --- a/API/Data/Migrations/20210322212724_MangaFileToPages.Designer.cs +++ b/Kavita.Database/Migrations/20210322212724_MangaFileToPages.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210322212724_MangaFileToPages")] diff --git a/API/Data/Migrations/20210322212724_MangaFileToPages.cs b/Kavita.Database/Migrations/20210322212724_MangaFileToPages.cs similarity index 94% rename from API/Data/Migrations/20210322212724_MangaFileToPages.cs rename to Kavita.Database/Migrations/20210322212724_MangaFileToPages.cs index 63fecfb72..0430f0c32 100644 --- a/API/Data/Migrations/20210322212724_MangaFileToPages.cs +++ b/Kavita.Database/Migrations/20210322212724_MangaFileToPages.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class MangaFileToPages : Migration { diff --git a/API/Data/Migrations/20210323213507_LastModifiedOnMangaFiles.Designer.cs b/Kavita.Database/Migrations/20210323213507_LastModifiedOnMangaFiles.Designer.cs similarity index 99% rename from API/Data/Migrations/20210323213507_LastModifiedOnMangaFiles.Designer.cs rename to Kavita.Database/Migrations/20210323213507_LastModifiedOnMangaFiles.Designer.cs index 1da79f6f7..c1459b34d 100644 --- a/API/Data/Migrations/20210323213507_LastModifiedOnMangaFiles.Designer.cs +++ b/Kavita.Database/Migrations/20210323213507_LastModifiedOnMangaFiles.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210323213507_LastModifiedOnMangaFiles")] diff --git a/API/Data/Migrations/20210323213507_LastModifiedOnMangaFiles.cs b/Kavita.Database/Migrations/20210323213507_LastModifiedOnMangaFiles.cs similarity index 95% rename from API/Data/Migrations/20210323213507_LastModifiedOnMangaFiles.cs rename to Kavita.Database/Migrations/20210323213507_LastModifiedOnMangaFiles.cs index 854498896..7a7ec67d5 100644 --- a/API/Data/Migrations/20210323213507_LastModifiedOnMangaFiles.cs +++ b/Kavita.Database/Migrations/20210323213507_LastModifiedOnMangaFiles.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class LastModifiedOnMangaFiles : Migration { diff --git a/API/Data/Migrations/20210330134414_IsSpecialOnChapters.Designer.cs b/Kavita.Database/Migrations/20210330134414_IsSpecialOnChapters.Designer.cs similarity index 99% rename from API/Data/Migrations/20210330134414_IsSpecialOnChapters.Designer.cs rename to Kavita.Database/Migrations/20210330134414_IsSpecialOnChapters.Designer.cs index 910085fd2..ce2533aad 100644 --- a/API/Data/Migrations/20210330134414_IsSpecialOnChapters.Designer.cs +++ b/Kavita.Database/Migrations/20210330134414_IsSpecialOnChapters.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210330134414_IsSpecialOnChapters")] diff --git a/API/Data/Migrations/20210330134414_IsSpecialOnChapters.cs b/Kavita.Database/Migrations/20210330134414_IsSpecialOnChapters.cs similarity index 94% rename from API/Data/Migrations/20210330134414_IsSpecialOnChapters.cs rename to Kavita.Database/Migrations/20210330134414_IsSpecialOnChapters.cs index 6653a0b77..bc5a5e738 100644 --- a/API/Data/Migrations/20210330134414_IsSpecialOnChapters.cs +++ b/Kavita.Database/Migrations/20210330134414_IsSpecialOnChapters.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class IsSpecialOnChapters : Migration { diff --git a/API/Data/Migrations/20210419222000_BookReaderPreferences.Designer.cs b/Kavita.Database/Migrations/20210419222000_BookReaderPreferences.Designer.cs similarity index 99% rename from API/Data/Migrations/20210419222000_BookReaderPreferences.Designer.cs rename to Kavita.Database/Migrations/20210419222000_BookReaderPreferences.Designer.cs index eb4dd459a..7941f4dba 100644 --- a/API/Data/Migrations/20210419222000_BookReaderPreferences.Designer.cs +++ b/Kavita.Database/Migrations/20210419222000_BookReaderPreferences.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210419222000_BookReaderPreferences")] diff --git a/API/Data/Migrations/20210419222000_BookReaderPreferences.cs b/Kavita.Database/Migrations/20210419222000_BookReaderPreferences.cs similarity index 97% rename from API/Data/Migrations/20210419222000_BookReaderPreferences.cs rename to Kavita.Database/Migrations/20210419222000_BookReaderPreferences.cs index 0dd1089eb..b474815d7 100644 --- a/API/Data/Migrations/20210419222000_BookReaderPreferences.cs +++ b/Kavita.Database/Migrations/20210419222000_BookReaderPreferences.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class BookReaderPreferences : Migration { diff --git a/API/Data/Migrations/20210419234652_BookReaderPreferencesFontSize.Designer.cs b/Kavita.Database/Migrations/20210419234652_BookReaderPreferencesFontSize.Designer.cs similarity index 99% rename from API/Data/Migrations/20210419234652_BookReaderPreferencesFontSize.Designer.cs rename to Kavita.Database/Migrations/20210419234652_BookReaderPreferencesFontSize.Designer.cs index 95005cf47..462919507 100644 --- a/API/Data/Migrations/20210419234652_BookReaderPreferencesFontSize.Designer.cs +++ b/Kavita.Database/Migrations/20210419234652_BookReaderPreferencesFontSize.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210419234652_BookReaderPreferencesFontSize")] diff --git a/API/Data/Migrations/20210419234652_BookReaderPreferencesFontSize.cs b/Kavita.Database/Migrations/20210419234652_BookReaderPreferencesFontSize.cs similarity index 94% rename from API/Data/Migrations/20210419234652_BookReaderPreferencesFontSize.cs rename to Kavita.Database/Migrations/20210419234652_BookReaderPreferencesFontSize.cs index 1745e4f73..e869e46e3 100644 --- a/API/Data/Migrations/20210419234652_BookReaderPreferencesFontSize.cs +++ b/Kavita.Database/Migrations/20210419234652_BookReaderPreferencesFontSize.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class BookReaderPreferencesFontSize : Migration { diff --git a/API/Data/Migrations/20210423132900_CustomChapterTitle.Designer.cs b/Kavita.Database/Migrations/20210423132900_CustomChapterTitle.Designer.cs similarity index 99% rename from API/Data/Migrations/20210423132900_CustomChapterTitle.Designer.cs rename to Kavita.Database/Migrations/20210423132900_CustomChapterTitle.Designer.cs index 693480dd3..656cde11f 100644 --- a/API/Data/Migrations/20210423132900_CustomChapterTitle.Designer.cs +++ b/Kavita.Database/Migrations/20210423132900_CustomChapterTitle.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210423132900_CustomChapterTitle")] diff --git a/API/Data/Migrations/20210423132900_CustomChapterTitle.cs b/Kavita.Database/Migrations/20210423132900_CustomChapterTitle.cs similarity index 96% rename from API/Data/Migrations/20210423132900_CustomChapterTitle.cs rename to Kavita.Database/Migrations/20210423132900_CustomChapterTitle.cs index b3958127c..9ccc413c4 100644 --- a/API/Data/Migrations/20210423132900_CustomChapterTitle.cs +++ b/Kavita.Database/Migrations/20210423132900_CustomChapterTitle.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class CustomChapterTitle : Migration { diff --git a/API/Data/Migrations/20210504184715_TapToPaginatePref.Designer.cs b/Kavita.Database/Migrations/20210504184715_TapToPaginatePref.Designer.cs similarity index 99% rename from API/Data/Migrations/20210504184715_TapToPaginatePref.Designer.cs rename to Kavita.Database/Migrations/20210504184715_TapToPaginatePref.Designer.cs index 86db800ce..ef255d387 100644 --- a/API/Data/Migrations/20210504184715_TapToPaginatePref.Designer.cs +++ b/Kavita.Database/Migrations/20210504184715_TapToPaginatePref.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210504184715_TapToPaginatePref")] diff --git a/API/Data/Migrations/20210504184715_TapToPaginatePref.cs b/Kavita.Database/Migrations/20210504184715_TapToPaginatePref.cs similarity index 94% rename from API/Data/Migrations/20210504184715_TapToPaginatePref.cs rename to Kavita.Database/Migrations/20210504184715_TapToPaginatePref.cs index c1f86ee4b..76bd386a6 100644 --- a/API/Data/Migrations/20210504184715_TapToPaginatePref.cs +++ b/Kavita.Database/Migrations/20210504184715_TapToPaginatePref.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class TapToPaginatePref : Migration { diff --git a/API/Data/Migrations/20210509014029_SiteDarkModePreference.Designer.cs b/Kavita.Database/Migrations/20210509014029_SiteDarkModePreference.Designer.cs similarity index 99% rename from API/Data/Migrations/20210509014029_SiteDarkModePreference.Designer.cs rename to Kavita.Database/Migrations/20210509014029_SiteDarkModePreference.Designer.cs index a33cd0809..cce558a2c 100644 --- a/API/Data/Migrations/20210509014029_SiteDarkModePreference.Designer.cs +++ b/Kavita.Database/Migrations/20210509014029_SiteDarkModePreference.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210509014029_SiteDarkModePreference")] diff --git a/API/Data/Migrations/20210509014029_SiteDarkModePreference.cs b/Kavita.Database/Migrations/20210509014029_SiteDarkModePreference.cs similarity index 94% rename from API/Data/Migrations/20210509014029_SiteDarkModePreference.cs rename to Kavita.Database/Migrations/20210509014029_SiteDarkModePreference.cs index 863eea564..83c2784f1 100644 --- a/API/Data/Migrations/20210509014029_SiteDarkModePreference.cs +++ b/Kavita.Database/Migrations/20210509014029_SiteDarkModePreference.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SiteDarkModePreference : Migration { diff --git a/API/Data/Migrations/20210519215934_CollectionTag.Designer.cs b/Kavita.Database/Migrations/20210519215934_CollectionTag.Designer.cs similarity index 99% rename from API/Data/Migrations/20210519215934_CollectionTag.Designer.cs rename to Kavita.Database/Migrations/20210519215934_CollectionTag.Designer.cs index 17c4ec353..46bd40f28 100644 --- a/API/Data/Migrations/20210519215934_CollectionTag.Designer.cs +++ b/Kavita.Database/Migrations/20210519215934_CollectionTag.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210519215934_CollectionTag")] diff --git a/API/Data/Migrations/20210519215934_CollectionTag.cs b/Kavita.Database/Migrations/20210519215934_CollectionTag.cs similarity index 98% rename from API/Data/Migrations/20210519215934_CollectionTag.cs rename to Kavita.Database/Migrations/20210519215934_CollectionTag.cs index b95a3bd9b..fdfb836fd 100644 --- a/API/Data/Migrations/20210519215934_CollectionTag.cs +++ b/Kavita.Database/Migrations/20210519215934_CollectionTag.cs @@ -1,8 +1,7 @@ -using API.Entities; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class CollectionTag : Migration { diff --git a/API/Data/Migrations/20210528150353_CollectionCoverImage.Designer.cs b/Kavita.Database/Migrations/20210528150353_CollectionCoverImage.Designer.cs similarity index 99% rename from API/Data/Migrations/20210528150353_CollectionCoverImage.Designer.cs rename to Kavita.Database/Migrations/20210528150353_CollectionCoverImage.Designer.cs index b3d4c3d4a..20a22afd9 100644 --- a/API/Data/Migrations/20210528150353_CollectionCoverImage.Designer.cs +++ b/Kavita.Database/Migrations/20210528150353_CollectionCoverImage.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210528150353_CollectionCoverImage")] diff --git a/API/Data/Migrations/20210528150353_CollectionCoverImage.cs b/Kavita.Database/Migrations/20210528150353_CollectionCoverImage.cs similarity index 94% rename from API/Data/Migrations/20210528150353_CollectionCoverImage.cs rename to Kavita.Database/Migrations/20210528150353_CollectionCoverImage.cs index a38f8cf93..8f97e982b 100644 --- a/API/Data/Migrations/20210528150353_CollectionCoverImage.cs +++ b/Kavita.Database/Migrations/20210528150353_CollectionCoverImage.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class CollectionCoverImage : Migration { diff --git a/API/Data/Migrations/20210530201541_CollectionSummary.Designer.cs b/Kavita.Database/Migrations/20210530201541_CollectionSummary.Designer.cs similarity index 99% rename from API/Data/Migrations/20210530201541_CollectionSummary.Designer.cs rename to Kavita.Database/Migrations/20210530201541_CollectionSummary.Designer.cs index 9d5507b38..4f03a3e5e 100644 --- a/API/Data/Migrations/20210530201541_CollectionSummary.Designer.cs +++ b/Kavita.Database/Migrations/20210530201541_CollectionSummary.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210530201541_CollectionSummary")] diff --git a/API/Data/Migrations/20210530201541_CollectionSummary.cs b/Kavita.Database/Migrations/20210530201541_CollectionSummary.cs similarity index 94% rename from API/Data/Migrations/20210530201541_CollectionSummary.cs rename to Kavita.Database/Migrations/20210530201541_CollectionSummary.cs index 255ad78f3..321697b59 100644 --- a/API/Data/Migrations/20210530201541_CollectionSummary.cs +++ b/Kavita.Database/Migrations/20210530201541_CollectionSummary.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class CollectionSummary : Migration { diff --git a/API/Data/Migrations/20210603133957_BookReadingDirectionPref.Designer.cs b/Kavita.Database/Migrations/20210603133957_BookReadingDirectionPref.Designer.cs similarity index 99% rename from API/Data/Migrations/20210603133957_BookReadingDirectionPref.Designer.cs rename to Kavita.Database/Migrations/20210603133957_BookReadingDirectionPref.Designer.cs index 2ef682c3c..acac8d1a0 100644 --- a/API/Data/Migrations/20210603133957_BookReadingDirectionPref.Designer.cs +++ b/Kavita.Database/Migrations/20210603133957_BookReadingDirectionPref.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210603133957_BookReadingDirectionPref")] diff --git a/API/Data/Migrations/20210603133957_BookReadingDirectionPref.cs b/Kavita.Database/Migrations/20210603133957_BookReadingDirectionPref.cs similarity index 94% rename from API/Data/Migrations/20210603133957_BookReadingDirectionPref.cs rename to Kavita.Database/Migrations/20210603133957_BookReadingDirectionPref.cs index 9f2d9760e..19f527a83 100644 --- a/API/Data/Migrations/20210603133957_BookReadingDirectionPref.cs +++ b/Kavita.Database/Migrations/20210603133957_BookReadingDirectionPref.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class BookReadingDirectionPref : Migration { diff --git a/API/Data/Migrations/20210603212429_BookScrollIdProgress.Designer.cs b/Kavita.Database/Migrations/20210603212429_BookScrollIdProgress.Designer.cs similarity index 99% rename from API/Data/Migrations/20210603212429_BookScrollIdProgress.Designer.cs rename to Kavita.Database/Migrations/20210603212429_BookScrollIdProgress.Designer.cs index 01a7c07a1..c8bf2fb37 100644 --- a/API/Data/Migrations/20210603212429_BookScrollIdProgress.Designer.cs +++ b/Kavita.Database/Migrations/20210603212429_BookScrollIdProgress.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210603212429_BookScrollIdProgress")] diff --git a/API/Data/Migrations/20210603212429_BookScrollIdProgress.cs b/Kavita.Database/Migrations/20210603212429_BookScrollIdProgress.cs similarity index 94% rename from API/Data/Migrations/20210603212429_BookScrollIdProgress.cs rename to Kavita.Database/Migrations/20210603212429_BookScrollIdProgress.cs index f2be301fe..a841c3890 100644 --- a/API/Data/Migrations/20210603212429_BookScrollIdProgress.cs +++ b/Kavita.Database/Migrations/20210603212429_BookScrollIdProgress.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class BookScrollIdProgress : Migration { diff --git a/API/Data/Migrations/20210622164318_NewUserPreferences.Designer.cs b/Kavita.Database/Migrations/20210622164318_NewUserPreferences.Designer.cs similarity index 99% rename from API/Data/Migrations/20210622164318_NewUserPreferences.Designer.cs rename to Kavita.Database/Migrations/20210622164318_NewUserPreferences.Designer.cs index 2797f05ab..3eb7f4d62 100644 --- a/API/Data/Migrations/20210622164318_NewUserPreferences.Designer.cs +++ b/Kavita.Database/Migrations/20210622164318_NewUserPreferences.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210622164318_NewUserPreferences")] diff --git a/API/Data/Migrations/20210622164318_NewUserPreferences.cs b/Kavita.Database/Migrations/20210622164318_NewUserPreferences.cs similarity index 96% rename from API/Data/Migrations/20210622164318_NewUserPreferences.cs rename to Kavita.Database/Migrations/20210622164318_NewUserPreferences.cs index bd75d5b2c..f697206c2 100644 --- a/API/Data/Migrations/20210622164318_NewUserPreferences.cs +++ b/Kavita.Database/Migrations/20210622164318_NewUserPreferences.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class NewUserPreferences : Migration { diff --git a/API/Data/Migrations/20210722223304_AddedSeriesFormat.Designer.cs b/Kavita.Database/Migrations/20210722223304_AddedSeriesFormat.Designer.cs similarity index 99% rename from API/Data/Migrations/20210722223304_AddedSeriesFormat.Designer.cs rename to Kavita.Database/Migrations/20210722223304_AddedSeriesFormat.Designer.cs index dff2d3868..df99a1d17 100644 --- a/API/Data/Migrations/20210722223304_AddedSeriesFormat.Designer.cs +++ b/Kavita.Database/Migrations/20210722223304_AddedSeriesFormat.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210722223304_AddedSeriesFormat")] diff --git a/API/Data/Migrations/20210722223304_AddedSeriesFormat.cs b/Kavita.Database/Migrations/20210722223304_AddedSeriesFormat.cs similarity index 97% rename from API/Data/Migrations/20210722223304_AddedSeriesFormat.cs rename to Kavita.Database/Migrations/20210722223304_AddedSeriesFormat.cs index f236b6ec2..1c007b5a6 100644 --- a/API/Data/Migrations/20210722223304_AddedSeriesFormat.cs +++ b/Kavita.Database/Migrations/20210722223304_AddedSeriesFormat.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class AddedSeriesFormat : Migration { diff --git a/API/Data/Migrations/20210809210326_BookmarkPages.Designer.cs b/Kavita.Database/Migrations/20210809210326_BookmarkPages.Designer.cs similarity index 99% rename from API/Data/Migrations/20210809210326_BookmarkPages.Designer.cs rename to Kavita.Database/Migrations/20210809210326_BookmarkPages.Designer.cs index b339bbd99..eeb3eb7e7 100644 --- a/API/Data/Migrations/20210809210326_BookmarkPages.Designer.cs +++ b/Kavita.Database/Migrations/20210809210326_BookmarkPages.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210809210326_BookmarkPages")] diff --git a/API/Data/Migrations/20210809210326_BookmarkPages.cs b/Kavita.Database/Migrations/20210809210326_BookmarkPages.cs similarity index 97% rename from API/Data/Migrations/20210809210326_BookmarkPages.cs rename to Kavita.Database/Migrations/20210809210326_BookmarkPages.cs index 0ae48eeed..8c2bb29cd 100644 --- a/API/Data/Migrations/20210809210326_BookmarkPages.cs +++ b/Kavita.Database/Migrations/20210809210326_BookmarkPages.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class BookmarkPages : Migration { diff --git a/API/Data/Migrations/20210813010210_CoverImageLockFieldsPart1.Designer.cs b/Kavita.Database/Migrations/20210813010210_CoverImageLockFieldsPart1.Designer.cs similarity index 99% rename from API/Data/Migrations/20210813010210_CoverImageLockFieldsPart1.Designer.cs rename to Kavita.Database/Migrations/20210813010210_CoverImageLockFieldsPart1.Designer.cs index 991c616fc..64f296a44 100644 --- a/API/Data/Migrations/20210813010210_CoverImageLockFieldsPart1.Designer.cs +++ b/Kavita.Database/Migrations/20210813010210_CoverImageLockFieldsPart1.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210813010210_CoverImageLockFieldsPart1")] diff --git a/API/Data/Migrations/20210813010210_CoverImageLockFieldsPart1.cs b/Kavita.Database/Migrations/20210813010210_CoverImageLockFieldsPart1.cs similarity index 96% rename from API/Data/Migrations/20210813010210_CoverImageLockFieldsPart1.cs rename to Kavita.Database/Migrations/20210813010210_CoverImageLockFieldsPart1.cs index 1b04826cd..bd736c5dc 100644 --- a/API/Data/Migrations/20210813010210_CoverImageLockFieldsPart1.cs +++ b/Kavita.Database/Migrations/20210813010210_CoverImageLockFieldsPart1.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class CoverImageLockFieldsPart1 : Migration { diff --git a/API/Data/Migrations/20210814215831_CoverImageLockedFieldsPart2.Designer.cs b/Kavita.Database/Migrations/20210814215831_CoverImageLockedFieldsPart2.Designer.cs similarity index 99% rename from API/Data/Migrations/20210814215831_CoverImageLockedFieldsPart2.Designer.cs rename to Kavita.Database/Migrations/20210814215831_CoverImageLockedFieldsPart2.Designer.cs index a7d6f8afe..e7d1b0adc 100644 --- a/API/Data/Migrations/20210814215831_CoverImageLockedFieldsPart2.Designer.cs +++ b/Kavita.Database/Migrations/20210814215831_CoverImageLockedFieldsPart2.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210814215831_CoverImageLockedFieldsPart2")] diff --git a/API/Data/Migrations/20210814215831_CoverImageLockedFieldsPart2.cs b/Kavita.Database/Migrations/20210814215831_CoverImageLockedFieldsPart2.cs similarity index 94% rename from API/Data/Migrations/20210814215831_CoverImageLockedFieldsPart2.cs rename to Kavita.Database/Migrations/20210814215831_CoverImageLockedFieldsPart2.cs index 1c3ac713a..2d564d49b 100644 --- a/API/Data/Migrations/20210814215831_CoverImageLockedFieldsPart2.cs +++ b/Kavita.Database/Migrations/20210814215831_CoverImageLockedFieldsPart2.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class CoverImageLockedFieldsPart2 : Migration { diff --git a/API/Data/Migrations/20210817152226_ProgressConcurencyCheck.Designer.cs b/Kavita.Database/Migrations/20210817152226_ProgressConcurencyCheck.Designer.cs similarity index 99% rename from API/Data/Migrations/20210817152226_ProgressConcurencyCheck.Designer.cs rename to Kavita.Database/Migrations/20210817152226_ProgressConcurencyCheck.Designer.cs index 830e86064..bc54e43c4 100644 --- a/API/Data/Migrations/20210817152226_ProgressConcurencyCheck.Designer.cs +++ b/Kavita.Database/Migrations/20210817152226_ProgressConcurencyCheck.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210817152226_ProgressConcurencyCheck")] diff --git a/API/Data/Migrations/20210817152226_ProgressConcurencyCheck.cs b/Kavita.Database/Migrations/20210817152226_ProgressConcurencyCheck.cs similarity index 94% rename from API/Data/Migrations/20210817152226_ProgressConcurencyCheck.cs rename to Kavita.Database/Migrations/20210817152226_ProgressConcurencyCheck.cs index d6ec6aba9..97d366241 100644 --- a/API/Data/Migrations/20210817152226_ProgressConcurencyCheck.cs +++ b/Kavita.Database/Migrations/20210817152226_ProgressConcurencyCheck.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ProgressConcurencyCheck : Migration { diff --git a/API/Data/Migrations/20210826203258_userApiKey.Designer.cs b/Kavita.Database/Migrations/20210826203258_userApiKey.Designer.cs similarity index 99% rename from API/Data/Migrations/20210826203258_userApiKey.Designer.cs rename to Kavita.Database/Migrations/20210826203258_userApiKey.Designer.cs index ece3e3dec..59195994c 100644 --- a/API/Data/Migrations/20210826203258_userApiKey.Designer.cs +++ b/Kavita.Database/Migrations/20210826203258_userApiKey.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210826203258_userApiKey")] diff --git a/API/Data/Migrations/20210826203258_userApiKey.cs b/Kavita.Database/Migrations/20210826203258_userApiKey.cs similarity index 94% rename from API/Data/Migrations/20210826203258_userApiKey.cs rename to Kavita.Database/Migrations/20210826203258_userApiKey.cs index 5f95a253d..283f2dde9 100644 --- a/API/Data/Migrations/20210826203258_userApiKey.cs +++ b/Kavita.Database/Migrations/20210826203258_userApiKey.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class userApiKey : Migration { diff --git a/API/Data/Migrations/20210901150310_ReadingLists.Designer.cs b/Kavita.Database/Migrations/20210901150310_ReadingLists.Designer.cs similarity index 99% rename from API/Data/Migrations/20210901150310_ReadingLists.Designer.cs rename to Kavita.Database/Migrations/20210901150310_ReadingLists.Designer.cs index fef65fdcf..16254d8a1 100644 --- a/API/Data/Migrations/20210901150310_ReadingLists.Designer.cs +++ b/Kavita.Database/Migrations/20210901150310_ReadingLists.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210901150310_ReadingLists")] diff --git a/API/Data/Migrations/20210901150310_ReadingLists.cs b/Kavita.Database/Migrations/20210901150310_ReadingLists.cs similarity index 99% rename from API/Data/Migrations/20210901150310_ReadingLists.cs rename to Kavita.Database/Migrations/20210901150310_ReadingLists.cs index 709d3e17a..4a3b43a7a 100644 --- a/API/Data/Migrations/20210901150310_ReadingLists.cs +++ b/Kavita.Database/Migrations/20210901150310_ReadingLists.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ReadingLists : Migration { diff --git a/API/Data/Migrations/20210901200442_ReadingListsAdditions.Designer.cs b/Kavita.Database/Migrations/20210901200442_ReadingListsAdditions.Designer.cs similarity index 99% rename from API/Data/Migrations/20210901200442_ReadingListsAdditions.Designer.cs rename to Kavita.Database/Migrations/20210901200442_ReadingListsAdditions.Designer.cs index 8ee5bdec8..b6fec1dee 100644 --- a/API/Data/Migrations/20210901200442_ReadingListsAdditions.Designer.cs +++ b/Kavita.Database/Migrations/20210901200442_ReadingListsAdditions.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210901200442_ReadingListsAdditions")] diff --git a/API/Data/Migrations/20210901200442_ReadingListsAdditions.cs b/Kavita.Database/Migrations/20210901200442_ReadingListsAdditions.cs similarity index 98% rename from API/Data/Migrations/20210901200442_ReadingListsAdditions.cs rename to Kavita.Database/Migrations/20210901200442_ReadingListsAdditions.cs index b44c2ac4d..b9bf473b0 100644 --- a/API/Data/Migrations/20210901200442_ReadingListsAdditions.cs +++ b/Kavita.Database/Migrations/20210901200442_ReadingListsAdditions.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ReadingListsAdditions : Migration { diff --git a/API/Data/Migrations/20210902110705_ReadingListsExtraRealationships.Designer.cs b/Kavita.Database/Migrations/20210902110705_ReadingListsExtraRealationships.Designer.cs similarity index 99% rename from API/Data/Migrations/20210902110705_ReadingListsExtraRealationships.Designer.cs rename to Kavita.Database/Migrations/20210902110705_ReadingListsExtraRealationships.Designer.cs index 566d2c5be..feefa5ea0 100644 --- a/API/Data/Migrations/20210902110705_ReadingListsExtraRealationships.Designer.cs +++ b/Kavita.Database/Migrations/20210902110705_ReadingListsExtraRealationships.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210902110705_ReadingListsExtraRealationships")] diff --git a/API/Data/Migrations/20210902110705_ReadingListsExtraRealationships.cs b/Kavita.Database/Migrations/20210902110705_ReadingListsExtraRealationships.cs similarity index 98% rename from API/Data/Migrations/20210902110705_ReadingListsExtraRealationships.cs rename to Kavita.Database/Migrations/20210902110705_ReadingListsExtraRealationships.cs index 9ddb1b5fc..8d5333439 100644 --- a/API/Data/Migrations/20210902110705_ReadingListsExtraRealationships.cs +++ b/Kavita.Database/Migrations/20210902110705_ReadingListsExtraRealationships.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ReadingListsExtraRealationships : Migration { diff --git a/API/Data/Migrations/20210906140845_ReadingListsChanges.Designer.cs b/Kavita.Database/Migrations/20210906140845_ReadingListsChanges.Designer.cs similarity index 99% rename from API/Data/Migrations/20210906140845_ReadingListsChanges.Designer.cs rename to Kavita.Database/Migrations/20210906140845_ReadingListsChanges.Designer.cs index 836a496e0..f8655884f 100644 --- a/API/Data/Migrations/20210906140845_ReadingListsChanges.Designer.cs +++ b/Kavita.Database/Migrations/20210906140845_ReadingListsChanges.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210906140845_ReadingListsChanges")] diff --git a/API/Data/Migrations/20210906140845_ReadingListsChanges.cs b/Kavita.Database/Migrations/20210906140845_ReadingListsChanges.cs similarity index 97% rename from API/Data/Migrations/20210906140845_ReadingListsChanges.cs rename to Kavita.Database/Migrations/20210906140845_ReadingListsChanges.cs index e4ea07e2e..6a9875afd 100644 --- a/API/Data/Migrations/20210906140845_ReadingListsChanges.cs +++ b/Kavita.Database/Migrations/20210906140845_ReadingListsChanges.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ReadingListsChanges : Migration { diff --git a/API/Data/Migrations/20210916142418_EntityImageRefactor.Designer.cs b/Kavita.Database/Migrations/20210916142418_EntityImageRefactor.Designer.cs similarity index 99% rename from API/Data/Migrations/20210916142418_EntityImageRefactor.Designer.cs rename to Kavita.Database/Migrations/20210916142418_EntityImageRefactor.Designer.cs index b4c6f62f1..1533122d6 100644 --- a/API/Data/Migrations/20210916142418_EntityImageRefactor.Designer.cs +++ b/Kavita.Database/Migrations/20210916142418_EntityImageRefactor.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20210916142418_EntityImageRefactor")] diff --git a/API/Data/Migrations/20210916142418_EntityImageRefactor.cs b/Kavita.Database/Migrations/20210916142418_EntityImageRefactor.cs similarity index 98% rename from API/Data/Migrations/20210916142418_EntityImageRefactor.cs rename to Kavita.Database/Migrations/20210916142418_EntityImageRefactor.cs index deafb134b..93dc4196c 100644 --- a/API/Data/Migrations/20210916142418_EntityImageRefactor.cs +++ b/Kavita.Database/Migrations/20210916142418_EntityImageRefactor.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class EntityImageRefactor : Migration { diff --git a/API/Data/Migrations/20211001113608_LastScannedLibrary.Designer.cs b/Kavita.Database/Migrations/20211001113608_LastScannedLibrary.Designer.cs similarity index 99% rename from API/Data/Migrations/20211001113608_LastScannedLibrary.Designer.cs rename to Kavita.Database/Migrations/20211001113608_LastScannedLibrary.Designer.cs index ad28c5839..2ad96ac03 100644 --- a/API/Data/Migrations/20211001113608_LastScannedLibrary.Designer.cs +++ b/Kavita.Database/Migrations/20211001113608_LastScannedLibrary.Designer.cs @@ -1,12 +1,12 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20211001113608_LastScannedLibrary")] diff --git a/API/Data/Migrations/20211001113608_LastScannedLibrary.cs b/Kavita.Database/Migrations/20211001113608_LastScannedLibrary.cs similarity index 95% rename from API/Data/Migrations/20211001113608_LastScannedLibrary.cs rename to Kavita.Database/Migrations/20211001113608_LastScannedLibrary.cs index eb1fdc5cb..a0cd62ea3 100644 --- a/API/Data/Migrations/20211001113608_LastScannedLibrary.cs +++ b/Kavita.Database/Migrations/20211001113608_LastScannedLibrary.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class LastScannedLibrary : Migration { diff --git a/API/Data/Migrations/20211127200244_MetadataFoundation.Designer.cs b/Kavita.Database/Migrations/20211127200244_MetadataFoundation.Designer.cs similarity index 99% rename from API/Data/Migrations/20211127200244_MetadataFoundation.Designer.cs rename to Kavita.Database/Migrations/20211127200244_MetadataFoundation.Designer.cs index 32408164b..79ba9f209 100644 --- a/API/Data/Migrations/20211127200244_MetadataFoundation.Designer.cs +++ b/Kavita.Database/Migrations/20211127200244_MetadataFoundation.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20211127200244_MetadataFoundation")] diff --git a/API/Data/Migrations/20211127200244_MetadataFoundation.cs b/Kavita.Database/Migrations/20211127200244_MetadataFoundation.cs similarity index 99% rename from API/Data/Migrations/20211127200244_MetadataFoundation.cs rename to Kavita.Database/Migrations/20211127200244_MetadataFoundation.cs index f2ea2c9c1..d7142a62b 100644 --- a/API/Data/Migrations/20211127200244_MetadataFoundation.cs +++ b/Kavita.Database/Migrations/20211127200244_MetadataFoundation.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class MetadataFoundation : Migration { diff --git a/API/Data/Migrations/20211129231007_RemoveChapterMetadata.Designer.cs b/Kavita.Database/Migrations/20211129231007_RemoveChapterMetadata.Designer.cs similarity index 99% rename from API/Data/Migrations/20211129231007_RemoveChapterMetadata.Designer.cs rename to Kavita.Database/Migrations/20211129231007_RemoveChapterMetadata.Designer.cs index 27436b91f..5da3a2bfd 100644 --- a/API/Data/Migrations/20211129231007_RemoveChapterMetadata.Designer.cs +++ b/Kavita.Database/Migrations/20211129231007_RemoveChapterMetadata.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20211129231007_RemoveChapterMetadata")] diff --git a/API/Data/Migrations/20211129231007_RemoveChapterMetadata.cs b/Kavita.Database/Migrations/20211129231007_RemoveChapterMetadata.cs similarity index 99% rename from API/Data/Migrations/20211129231007_RemoveChapterMetadata.cs rename to Kavita.Database/Migrations/20211129231007_RemoveChapterMetadata.cs index c50578ff9..0691ef064 100644 --- a/API/Data/Migrations/20211129231007_RemoveChapterMetadata.cs +++ b/Kavita.Database/Migrations/20211129231007_RemoveChapterMetadata.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class RemoveChapterMetadata : Migration { diff --git a/API/Data/Migrations/20211130134642_GenreProvider.Designer.cs b/Kavita.Database/Migrations/20211130134642_GenreProvider.Designer.cs similarity index 99% rename from API/Data/Migrations/20211130134642_GenreProvider.Designer.cs rename to Kavita.Database/Migrations/20211130134642_GenreProvider.Designer.cs index 4b90e75ba..ae8d5cda8 100644 --- a/API/Data/Migrations/20211130134642_GenreProvider.Designer.cs +++ b/Kavita.Database/Migrations/20211130134642_GenreProvider.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20211130134642_GenreProvider")] diff --git a/API/Data/Migrations/20211130134642_GenreProvider.cs b/Kavita.Database/Migrations/20211130134642_GenreProvider.cs similarity index 98% rename from API/Data/Migrations/20211130134642_GenreProvider.cs rename to Kavita.Database/Migrations/20211130134642_GenreProvider.cs index 260210d54..d7a7ec4cf 100644 --- a/API/Data/Migrations/20211130134642_GenreProvider.cs +++ b/Kavita.Database/Migrations/20211130134642_GenreProvider.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class GenreProvider : Migration { diff --git a/API/Data/Migrations/20211201230003_GenreTitle.Designer.cs b/Kavita.Database/Migrations/20211201230003_GenreTitle.Designer.cs similarity index 99% rename from API/Data/Migrations/20211201230003_GenreTitle.Designer.cs rename to Kavita.Database/Migrations/20211201230003_GenreTitle.Designer.cs index 81f69b5a0..4292249c4 100644 --- a/API/Data/Migrations/20211201230003_GenreTitle.Designer.cs +++ b/Kavita.Database/Migrations/20211201230003_GenreTitle.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20211201230003_GenreTitle")] diff --git a/API/Data/Migrations/20211201230003_GenreTitle.cs b/Kavita.Database/Migrations/20211201230003_GenreTitle.cs similarity index 98% rename from API/Data/Migrations/20211201230003_GenreTitle.cs rename to Kavita.Database/Migrations/20211201230003_GenreTitle.cs index ab3e65daf..b5bf17387 100644 --- a/API/Data/Migrations/20211201230003_GenreTitle.cs +++ b/Kavita.Database/Migrations/20211201230003_GenreTitle.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class GenreTitle : Migration { diff --git a/API/Data/Migrations/20211205185207_MetadataAgeRating.Designer.cs b/Kavita.Database/Migrations/20211205185207_MetadataAgeRating.Designer.cs similarity index 99% rename from API/Data/Migrations/20211205185207_MetadataAgeRating.Designer.cs rename to Kavita.Database/Migrations/20211205185207_MetadataAgeRating.Designer.cs index 58704e29d..f8d928a33 100644 --- a/API/Data/Migrations/20211205185207_MetadataAgeRating.Designer.cs +++ b/Kavita.Database/Migrations/20211205185207_MetadataAgeRating.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20211205185207_MetadataAgeRating")] diff --git a/API/Data/Migrations/20211205185207_MetadataAgeRating.cs b/Kavita.Database/Migrations/20211205185207_MetadataAgeRating.cs similarity index 94% rename from API/Data/Migrations/20211205185207_MetadataAgeRating.cs rename to Kavita.Database/Migrations/20211205185207_MetadataAgeRating.cs index 8f03753f6..a9baefd12 100644 --- a/API/Data/Migrations/20211205185207_MetadataAgeRating.cs +++ b/Kavita.Database/Migrations/20211205185207_MetadataAgeRating.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class MetadataAgeRating : Migration { diff --git a/API/Data/Migrations/20211206193225_AgeRatingAndReleaseDate.Designer.cs b/Kavita.Database/Migrations/20211206193225_AgeRatingAndReleaseDate.Designer.cs similarity index 99% rename from API/Data/Migrations/20211206193225_AgeRatingAndReleaseDate.Designer.cs rename to Kavita.Database/Migrations/20211206193225_AgeRatingAndReleaseDate.Designer.cs index eade9e871..038c9fd40 100644 --- a/API/Data/Migrations/20211206193225_AgeRatingAndReleaseDate.Designer.cs +++ b/Kavita.Database/Migrations/20211206193225_AgeRatingAndReleaseDate.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20211206193225_AgeRatingAndReleaseDate")] diff --git a/API/Data/Migrations/20211206193225_AgeRatingAndReleaseDate.cs b/Kavita.Database/Migrations/20211206193225_AgeRatingAndReleaseDate.cs similarity index 97% rename from API/Data/Migrations/20211206193225_AgeRatingAndReleaseDate.cs rename to Kavita.Database/Migrations/20211206193225_AgeRatingAndReleaseDate.cs index 76a7f05c6..c05f7a013 100644 --- a/API/Data/Migrations/20211206193225_AgeRatingAndReleaseDate.cs +++ b/Kavita.Database/Migrations/20211206193225_AgeRatingAndReleaseDate.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class AgeRatingAndReleaseDate : Migration { diff --git a/API/Data/Migrations/20211217013734_BookmarkRefactor.Designer.cs b/Kavita.Database/Migrations/20211217013734_BookmarkRefactor.Designer.cs similarity index 99% rename from API/Data/Migrations/20211217013734_BookmarkRefactor.Designer.cs rename to Kavita.Database/Migrations/20211217013734_BookmarkRefactor.Designer.cs index 5db4111f6..253205078 100644 --- a/API/Data/Migrations/20211217013734_BookmarkRefactor.Designer.cs +++ b/Kavita.Database/Migrations/20211217013734_BookmarkRefactor.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20211217013734_BookmarkRefactor")] diff --git a/API/Data/Migrations/20211217013734_BookmarkRefactor.cs b/Kavita.Database/Migrations/20211217013734_BookmarkRefactor.cs similarity index 94% rename from API/Data/Migrations/20211217013734_BookmarkRefactor.cs rename to Kavita.Database/Migrations/20211217013734_BookmarkRefactor.cs index 7ac831e07..e5479e662 100644 --- a/API/Data/Migrations/20211217013734_BookmarkRefactor.cs +++ b/Kavita.Database/Migrations/20211217013734_BookmarkRefactor.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class BookmarkRefactor : Migration { diff --git a/API/Data/Migrations/20211217180457_filteringChanges.Designer.cs b/Kavita.Database/Migrations/20211217180457_filteringChanges.Designer.cs similarity index 99% rename from API/Data/Migrations/20211217180457_filteringChanges.Designer.cs rename to Kavita.Database/Migrations/20211217180457_filteringChanges.Designer.cs index 39377a6c3..ab6d1fed9 100644 --- a/API/Data/Migrations/20211217180457_filteringChanges.Designer.cs +++ b/Kavita.Database/Migrations/20211217180457_filteringChanges.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20211217180457_filteringChanges")] diff --git a/API/Data/Migrations/20211217180457_filteringChanges.cs b/Kavita.Database/Migrations/20211217180457_filteringChanges.cs similarity index 99% rename from API/Data/Migrations/20211217180457_filteringChanges.cs rename to Kavita.Database/Migrations/20211217180457_filteringChanges.cs index 28c4d00b3..4638ed986 100644 --- a/API/Data/Migrations/20211217180457_filteringChanges.cs +++ b/Kavita.Database/Migrations/20211217180457_filteringChanges.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class filteringChanges : Migration { diff --git a/API/Data/Migrations/20211227180752_FullscreenPref.Designer.cs b/Kavita.Database/Migrations/20211227180752_FullscreenPref.Designer.cs similarity index 99% rename from API/Data/Migrations/20211227180752_FullscreenPref.Designer.cs rename to Kavita.Database/Migrations/20211227180752_FullscreenPref.Designer.cs index b649b12b6..4673be8fc 100644 --- a/API/Data/Migrations/20211227180752_FullscreenPref.Designer.cs +++ b/Kavita.Database/Migrations/20211227180752_FullscreenPref.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20211227180752_FullscreenPref")] diff --git a/API/Data/Migrations/20211227180752_FullscreenPref.cs b/Kavita.Database/Migrations/20211227180752_FullscreenPref.cs similarity index 94% rename from API/Data/Migrations/20211227180752_FullscreenPref.cs rename to Kavita.Database/Migrations/20211227180752_FullscreenPref.cs index ab6cbc8a8..6a53dd414 100644 --- a/API/Data/Migrations/20211227180752_FullscreenPref.cs +++ b/Kavita.Database/Migrations/20211227180752_FullscreenPref.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class FullscreenPref : Migration { diff --git a/API/Data/Migrations/20220107232822_ChapterMetadataOptimization.Designer.cs b/Kavita.Database/Migrations/20220107232822_ChapterMetadataOptimization.Designer.cs similarity index 99% rename from API/Data/Migrations/20220107232822_ChapterMetadataOptimization.Designer.cs rename to Kavita.Database/Migrations/20220107232822_ChapterMetadataOptimization.Designer.cs index 9df425dbd..2beed06ab 100644 --- a/API/Data/Migrations/20220107232822_ChapterMetadataOptimization.Designer.cs +++ b/Kavita.Database/Migrations/20220107232822_ChapterMetadataOptimization.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220107232822_ChapterMetadataOptimization")] diff --git a/API/Data/Migrations/20220107232822_ChapterMetadataOptimization.cs b/Kavita.Database/Migrations/20220107232822_ChapterMetadataOptimization.cs similarity index 98% rename from API/Data/Migrations/20220107232822_ChapterMetadataOptimization.cs rename to Kavita.Database/Migrations/20220107232822_ChapterMetadataOptimization.cs index 28e874f03..4aaaf387b 100644 --- a/API/Data/Migrations/20220107232822_ChapterMetadataOptimization.cs +++ b/Kavita.Database/Migrations/20220107232822_ChapterMetadataOptimization.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ChapterMetadataOptimization : Migration { diff --git a/API/Data/Migrations/20220108200822_CountMetadata.Designer.cs b/Kavita.Database/Migrations/20220108200822_CountMetadata.Designer.cs similarity index 99% rename from API/Data/Migrations/20220108200822_CountMetadata.Designer.cs rename to Kavita.Database/Migrations/20220108200822_CountMetadata.Designer.cs index 1866b6e58..89da25f6b 100644 --- a/API/Data/Migrations/20220108200822_CountMetadata.Designer.cs +++ b/Kavita.Database/Migrations/20220108200822_CountMetadata.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220108200822_CountMetadata")] diff --git a/API/Data/Migrations/20220108200822_CountMetadata.cs b/Kavita.Database/Migrations/20220108200822_CountMetadata.cs similarity index 96% rename from API/Data/Migrations/20220108200822_CountMetadata.cs rename to Kavita.Database/Migrations/20220108200822_CountMetadata.cs index 98a7f7e11..26ade1010 100644 --- a/API/Data/Migrations/20220108200822_CountMetadata.cs +++ b/Kavita.Database/Migrations/20220108200822_CountMetadata.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class CountMetadata : Migration { diff --git a/API/Data/Migrations/20220108202027_PublicationStatus.Designer.cs b/Kavita.Database/Migrations/20220108202027_PublicationStatus.Designer.cs similarity index 99% rename from API/Data/Migrations/20220108202027_PublicationStatus.Designer.cs rename to Kavita.Database/Migrations/20220108202027_PublicationStatus.Designer.cs index 8479775bf..75b370181 100644 --- a/API/Data/Migrations/20220108202027_PublicationStatus.Designer.cs +++ b/Kavita.Database/Migrations/20220108202027_PublicationStatus.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220108202027_PublicationStatus")] diff --git a/API/Data/Migrations/20220108202027_PublicationStatus.cs b/Kavita.Database/Migrations/20220108202027_PublicationStatus.cs similarity index 96% rename from API/Data/Migrations/20220108202027_PublicationStatus.cs rename to Kavita.Database/Migrations/20220108202027_PublicationStatus.cs index a8d676ed0..f3b663546 100644 --- a/API/Data/Migrations/20220108202027_PublicationStatus.cs +++ b/Kavita.Database/Migrations/20220108202027_PublicationStatus.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class PublicationStatus : Migration { diff --git a/API/Data/Migrations/20220215163317_SiteTheme.Designer.cs b/Kavita.Database/Migrations/20220215163317_SiteTheme.Designer.cs similarity index 99% rename from API/Data/Migrations/20220215163317_SiteTheme.Designer.cs rename to Kavita.Database/Migrations/20220215163317_SiteTheme.Designer.cs index 43b538c9a..ebe3668b3 100644 --- a/API/Data/Migrations/20220215163317_SiteTheme.Designer.cs +++ b/Kavita.Database/Migrations/20220215163317_SiteTheme.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220215163317_SiteTheme")] diff --git a/API/Data/Migrations/20220215163317_SiteTheme.cs b/Kavita.Database/Migrations/20220215163317_SiteTheme.cs similarity index 98% rename from API/Data/Migrations/20220215163317_SiteTheme.cs rename to Kavita.Database/Migrations/20220215163317_SiteTheme.cs index e2f519f8b..1ee747ae7 100644 --- a/API/Data/Migrations/20220215163317_SiteTheme.cs +++ b/Kavita.Database/Migrations/20220215163317_SiteTheme.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SiteTheme : Migration { diff --git a/API/Data/Migrations/20220303205301_SeriesLockedFields.Designer.cs b/Kavita.Database/Migrations/20220303205301_SeriesLockedFields.Designer.cs similarity index 99% rename from API/Data/Migrations/20220303205301_SeriesLockedFields.Designer.cs rename to Kavita.Database/Migrations/20220303205301_SeriesLockedFields.Designer.cs index 00fc7a10f..c3e969abc 100644 --- a/API/Data/Migrations/20220303205301_SeriesLockedFields.Designer.cs +++ b/Kavita.Database/Migrations/20220303205301_SeriesLockedFields.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220303205301_SeriesLockedFields")] diff --git a/API/Data/Migrations/20220303205301_SeriesLockedFields.cs b/Kavita.Database/Migrations/20220303205301_SeriesLockedFields.cs similarity index 99% rename from API/Data/Migrations/20220303205301_SeriesLockedFields.cs rename to Kavita.Database/Migrations/20220303205301_SeriesLockedFields.cs index e3903db9e..d8cf5174e 100644 --- a/API/Data/Migrations/20220303205301_SeriesLockedFields.cs +++ b/Kavita.Database/Migrations/20220303205301_SeriesLockedFields.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SeriesLockedFields : Migration { diff --git a/API/Data/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.Designer.cs b/Kavita.Database/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.Designer.cs similarity index 99% rename from API/Data/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.Designer.cs rename to Kavita.Database/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.Designer.cs index a21ca1e92..ed165d128 100644 --- a/API/Data/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.Designer.cs +++ b/Kavita.Database/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220306155456_MangaReaderBackgroundAndLayoutMode")] diff --git a/API/Data/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.cs b/Kavita.Database/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.cs similarity index 93% rename from API/Data/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.cs rename to Kavita.Database/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.cs index 078e51684..9929e88a7 100644 --- a/API/Data/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.cs +++ b/Kavita.Database/Migrations/20220306155456_MangaReaderBackgroundAndLayoutMode.cs @@ -1,9 +1,9 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class MangaReaderBackgroundAndLayoutMode : Migration { diff --git a/API/Data/Migrations/20220307153053_ScreenHints.Designer.cs b/Kavita.Database/Migrations/20220307153053_ScreenHints.Designer.cs similarity index 99% rename from API/Data/Migrations/20220307153053_ScreenHints.Designer.cs rename to Kavita.Database/Migrations/20220307153053_ScreenHints.Designer.cs index f54b0ab0b..975ab51d4 100644 --- a/API/Data/Migrations/20220307153053_ScreenHints.Designer.cs +++ b/Kavita.Database/Migrations/20220307153053_ScreenHints.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220307153053_ScreenHints")] diff --git a/API/Data/Migrations/20220307153053_ScreenHints.cs b/Kavita.Database/Migrations/20220307153053_ScreenHints.cs similarity index 94% rename from API/Data/Migrations/20220307153053_ScreenHints.cs rename to Kavita.Database/Migrations/20220307153053_ScreenHints.cs index 6c7b67ade..a15a82d23 100644 --- a/API/Data/Migrations/20220307153053_ScreenHints.cs +++ b/Kavita.Database/Migrations/20220307153053_ScreenHints.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ScreenHints : Migration { diff --git a/API/Data/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.Designer.cs b/Kavita.Database/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.Designer.cs similarity index 99% rename from API/Data/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.Designer.cs rename to Kavita.Database/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.Designer.cs index 27d16bfde..9851710a6 100644 --- a/API/Data/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.Designer.cs +++ b/Kavita.Database/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220410230540_SeriesLastChapterAddedAndReadingListNormalization")] diff --git a/API/Data/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.cs b/Kavita.Database/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.cs similarity index 97% rename from API/Data/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.cs rename to Kavita.Database/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.cs index 445895472..b090a9d56 100644 --- a/API/Data/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.cs +++ b/Kavita.Database/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SeriesLastChapterAddedAndReadingListNormalization : Migration { diff --git a/API/Data/Migrations/20220416211340_RemoveCustomIndex.Designer.cs b/Kavita.Database/Migrations/20220416211340_RemoveCustomIndex.Designer.cs similarity index 99% rename from API/Data/Migrations/20220416211340_RemoveCustomIndex.Designer.cs rename to Kavita.Database/Migrations/20220416211340_RemoveCustomIndex.Designer.cs index dd2c6ce88..0bc554388 100644 --- a/API/Data/Migrations/20220416211340_RemoveCustomIndex.Designer.cs +++ b/Kavita.Database/Migrations/20220416211340_RemoveCustomIndex.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220416211340_RemoveCustomIndex")] diff --git a/API/Data/Migrations/20220416211340_RemoveCustomIndex.cs b/Kavita.Database/Migrations/20220416211340_RemoveCustomIndex.cs similarity index 95% rename from API/Data/Migrations/20220416211340_RemoveCustomIndex.cs rename to Kavita.Database/Migrations/20220416211340_RemoveCustomIndex.cs index eb60f2349..d5510636e 100644 --- a/API/Data/Migrations/20220416211340_RemoveCustomIndex.cs +++ b/Kavita.Database/Migrations/20220416211340_RemoveCustomIndex.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class RemoveCustomIndex : Migration { diff --git a/API/Data/Migrations/20220421214448_SeriesRelations.Designer.cs b/Kavita.Database/Migrations/20220421214448_SeriesRelations.Designer.cs similarity index 99% rename from API/Data/Migrations/20220421214448_SeriesRelations.Designer.cs rename to Kavita.Database/Migrations/20220421214448_SeriesRelations.Designer.cs index 11937eb15..cf928393f 100644 --- a/API/Data/Migrations/20220421214448_SeriesRelations.Designer.cs +++ b/Kavita.Database/Migrations/20220421214448_SeriesRelations.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220421214448_SeriesRelations")] diff --git a/API/Data/Migrations/20220421214448_SeriesRelations.cs b/Kavita.Database/Migrations/20220421214448_SeriesRelations.cs similarity index 98% rename from API/Data/Migrations/20220421214448_SeriesRelations.cs rename to Kavita.Database/Migrations/20220421214448_SeriesRelations.cs index 1f6d5d7ab..172eeb79a 100644 --- a/API/Data/Migrations/20220421214448_SeriesRelations.cs +++ b/Kavita.Database/Migrations/20220421214448_SeriesRelations.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SeriesRelations : Migration { diff --git a/API/Data/Migrations/20220425125505_ChangeCountToTotalCount.Designer.cs b/Kavita.Database/Migrations/20220425125505_ChangeCountToTotalCount.Designer.cs similarity index 99% rename from API/Data/Migrations/20220425125505_ChangeCountToTotalCount.Designer.cs rename to Kavita.Database/Migrations/20220425125505_ChangeCountToTotalCount.Designer.cs index 321cf7056..797771658 100644 --- a/API/Data/Migrations/20220425125505_ChangeCountToTotalCount.Designer.cs +++ b/Kavita.Database/Migrations/20220425125505_ChangeCountToTotalCount.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220425125505_ChangeCountToTotalCount")] diff --git a/API/Data/Migrations/20220425125505_ChangeCountToTotalCount.cs b/Kavita.Database/Migrations/20220425125505_ChangeCountToTotalCount.cs similarity index 97% rename from API/Data/Migrations/20220425125505_ChangeCountToTotalCount.cs rename to Kavita.Database/Migrations/20220425125505_ChangeCountToTotalCount.cs index 469430bc7..e132ba583 100644 --- a/API/Data/Migrations/20220425125505_ChangeCountToTotalCount.cs +++ b/Kavita.Database/Migrations/20220425125505_ChangeCountToTotalCount.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ChangeCountToTotalCount : Migration { diff --git a/API/Data/Migrations/20220425131122_AddMaxCountToSeriesMetadata.Designer.cs b/Kavita.Database/Migrations/20220425131122_AddMaxCountToSeriesMetadata.Designer.cs similarity index 99% rename from API/Data/Migrations/20220425131122_AddMaxCountToSeriesMetadata.Designer.cs rename to Kavita.Database/Migrations/20220425131122_AddMaxCountToSeriesMetadata.Designer.cs index 0580b7497..73e2adc2e 100644 --- a/API/Data/Migrations/20220425131122_AddMaxCountToSeriesMetadata.Designer.cs +++ b/Kavita.Database/Migrations/20220425131122_AddMaxCountToSeriesMetadata.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220425131122_AddMaxCountToSeriesMetadata")] diff --git a/API/Data/Migrations/20220425131122_AddMaxCountToSeriesMetadata.cs b/Kavita.Database/Migrations/20220425131122_AddMaxCountToSeriesMetadata.cs similarity index 94% rename from API/Data/Migrations/20220425131122_AddMaxCountToSeriesMetadata.cs rename to Kavita.Database/Migrations/20220425131122_AddMaxCountToSeriesMetadata.cs index 550dac20b..7c94b2077 100644 --- a/API/Data/Migrations/20220425131122_AddMaxCountToSeriesMetadata.cs +++ b/Kavita.Database/Migrations/20220425131122_AddMaxCountToSeriesMetadata.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class AddMaxCountToSeriesMetadata : Migration { diff --git a/API/Data/Migrations/20220508162841_BookReaderUpdate.Designer.cs b/Kavita.Database/Migrations/20220508162841_BookReaderUpdate.Designer.cs similarity index 99% rename from API/Data/Migrations/20220508162841_BookReaderUpdate.Designer.cs rename to Kavita.Database/Migrations/20220508162841_BookReaderUpdate.Designer.cs index b8e7c6082..44fe9f673 100644 --- a/API/Data/Migrations/20220508162841_BookReaderUpdate.Designer.cs +++ b/Kavita.Database/Migrations/20220508162841_BookReaderUpdate.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220508162841_BookReaderUpdate")] diff --git a/API/Data/Migrations/20220508162841_BookReaderUpdate.cs b/Kavita.Database/Migrations/20220508162841_BookReaderUpdate.cs similarity index 97% rename from API/Data/Migrations/20220508162841_BookReaderUpdate.cs rename to Kavita.Database/Migrations/20220508162841_BookReaderUpdate.cs index 6df40e5fd..6ffb2757b 100644 --- a/API/Data/Migrations/20220508162841_BookReaderUpdate.cs +++ b/Kavita.Database/Migrations/20220508162841_BookReaderUpdate.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class BookReaderUpdate : Migration { diff --git a/API/Data/Migrations/20220513234708_BookReaderImmersiveMode.Designer.cs b/Kavita.Database/Migrations/20220513234708_BookReaderImmersiveMode.Designer.cs similarity index 99% rename from API/Data/Migrations/20220513234708_BookReaderImmersiveMode.Designer.cs rename to Kavita.Database/Migrations/20220513234708_BookReaderImmersiveMode.Designer.cs index 26c9a1397..67929729d 100644 --- a/API/Data/Migrations/20220513234708_BookReaderImmersiveMode.Designer.cs +++ b/Kavita.Database/Migrations/20220513234708_BookReaderImmersiveMode.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220513234708_BookReaderImmersiveMode")] diff --git a/API/Data/Migrations/20220513234708_BookReaderImmersiveMode.cs b/Kavita.Database/Migrations/20220513234708_BookReaderImmersiveMode.cs similarity index 95% rename from API/Data/Migrations/20220513234708_BookReaderImmersiveMode.cs rename to Kavita.Database/Migrations/20220513234708_BookReaderImmersiveMode.cs index f194a3b87..cabd694eb 100644 --- a/API/Data/Migrations/20220513234708_BookReaderImmersiveMode.cs +++ b/Kavita.Database/Migrations/20220513234708_BookReaderImmersiveMode.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class BookReaderImmersiveMode : Migration { diff --git a/API/Data/Migrations/20220524172543_WordCount.Designer.cs b/Kavita.Database/Migrations/20220524172543_WordCount.Designer.cs similarity index 99% rename from API/Data/Migrations/20220524172543_WordCount.Designer.cs rename to Kavita.Database/Migrations/20220524172543_WordCount.Designer.cs index 04f2b5f38..6d51b6eaf 100644 --- a/API/Data/Migrations/20220524172543_WordCount.Designer.cs +++ b/Kavita.Database/Migrations/20220524172543_WordCount.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220524172543_WordCount")] diff --git a/API/Data/Migrations/20220524172543_WordCount.cs b/Kavita.Database/Migrations/20220524172543_WordCount.cs similarity index 96% rename from API/Data/Migrations/20220524172543_WordCount.cs rename to Kavita.Database/Migrations/20220524172543_WordCount.cs index 2828985b6..fbfcad99b 100644 --- a/API/Data/Migrations/20220524172543_WordCount.cs +++ b/Kavita.Database/Migrations/20220524172543_WordCount.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class WordCount : Migration { diff --git a/API/Data/Migrations/20220610153822_TimeEstimateInDB.Designer.cs b/Kavita.Database/Migrations/20220610153822_TimeEstimateInDB.Designer.cs similarity index 99% rename from API/Data/Migrations/20220610153822_TimeEstimateInDB.Designer.cs rename to Kavita.Database/Migrations/20220610153822_TimeEstimateInDB.Designer.cs index dc5cfc8f2..cbb8d21e8 100644 --- a/API/Data/Migrations/20220610153822_TimeEstimateInDB.Designer.cs +++ b/Kavita.Database/Migrations/20220610153822_TimeEstimateInDB.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220610153822_TimeEstimateInDB")] diff --git a/API/Data/Migrations/20220610153822_TimeEstimateInDB.cs b/Kavita.Database/Migrations/20220610153822_TimeEstimateInDB.cs similarity index 99% rename from API/Data/Migrations/20220610153822_TimeEstimateInDB.cs rename to Kavita.Database/Migrations/20220610153822_TimeEstimateInDB.cs index 9986cc909..7d408a956 100644 --- a/API/Data/Migrations/20220610153822_TimeEstimateInDB.cs +++ b/Kavita.Database/Migrations/20220610153822_TimeEstimateInDB.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class TimeEstimateInDB : Migration { diff --git a/API/Data/Migrations/20220613131125_RenamedBookReaderLayoutMode.Designer.cs b/Kavita.Database/Migrations/20220613131125_RenamedBookReaderLayoutMode.Designer.cs similarity index 99% rename from API/Data/Migrations/20220613131125_RenamedBookReaderLayoutMode.Designer.cs rename to Kavita.Database/Migrations/20220613131125_RenamedBookReaderLayoutMode.Designer.cs index 73e3a675a..28a66f168 100644 --- a/API/Data/Migrations/20220613131125_RenamedBookReaderLayoutMode.Designer.cs +++ b/Kavita.Database/Migrations/20220613131125_RenamedBookReaderLayoutMode.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220613131125_RenamedBookReaderLayoutMode")] diff --git a/API/Data/Migrations/20220613131125_RenamedBookReaderLayoutMode.cs b/Kavita.Database/Migrations/20220613131125_RenamedBookReaderLayoutMode.cs similarity index 94% rename from API/Data/Migrations/20220613131125_RenamedBookReaderLayoutMode.cs rename to Kavita.Database/Migrations/20220613131125_RenamedBookReaderLayoutMode.cs index c7a5c5c13..f4a88a92b 100644 --- a/API/Data/Migrations/20220613131125_RenamedBookReaderLayoutMode.cs +++ b/Kavita.Database/Migrations/20220613131125_RenamedBookReaderLayoutMode.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class RenamedBookReaderLayoutMode : Migration { diff --git a/API/Data/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.Designer.cs b/Kavita.Database/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.Designer.cs similarity index 99% rename from API/Data/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.Designer.cs rename to Kavita.Database/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.Designer.cs index 44545d8a6..165a4740e 100644 --- a/API/Data/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.Designer.cs +++ b/Kavita.Database/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220613131302_GlobalPageLayoutModeUserSetting")] diff --git a/API/Data/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.cs b/Kavita.Database/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.cs similarity index 95% rename from API/Data/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.cs rename to Kavita.Database/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.cs index 397f9a734..d8441b8ee 100644 --- a/API/Data/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.cs +++ b/Kavita.Database/Migrations/20220613131302_GlobalPageLayoutModeUserSetting.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class GlobalPageLayoutModeUserSetting : Migration { diff --git a/API/Data/Migrations/20220615190640_LastFileAnalysis.Designer.cs b/Kavita.Database/Migrations/20220615190640_LastFileAnalysis.Designer.cs similarity index 99% rename from API/Data/Migrations/20220615190640_LastFileAnalysis.Designer.cs rename to Kavita.Database/Migrations/20220615190640_LastFileAnalysis.Designer.cs index 4c5a53f7f..c3c402c5f 100644 --- a/API/Data/Migrations/20220615190640_LastFileAnalysis.Designer.cs +++ b/Kavita.Database/Migrations/20220615190640_LastFileAnalysis.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220615190640_LastFileAnalysis")] diff --git a/API/Data/Migrations/20220615190640_LastFileAnalysis.cs b/Kavita.Database/Migrations/20220615190640_LastFileAnalysis.cs similarity index 95% rename from API/Data/Migrations/20220615190640_LastFileAnalysis.cs rename to Kavita.Database/Migrations/20220615190640_LastFileAnalysis.cs index b1fac2ae4..8df889d66 100644 --- a/API/Data/Migrations/20220615190640_LastFileAnalysis.cs +++ b/Kavita.Database/Migrations/20220615190640_LastFileAnalysis.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class LastFileAnalysis : Migration { diff --git a/API/Data/Migrations/20220625215526_BlurUnreadSummaries.Designer.cs b/Kavita.Database/Migrations/20220625215526_BlurUnreadSummaries.Designer.cs similarity index 99% rename from API/Data/Migrations/20220625215526_BlurUnreadSummaries.Designer.cs rename to Kavita.Database/Migrations/20220625215526_BlurUnreadSummaries.Designer.cs index 4aa051023..f1903c8eb 100644 --- a/API/Data/Migrations/20220625215526_BlurUnreadSummaries.Designer.cs +++ b/Kavita.Database/Migrations/20220625215526_BlurUnreadSummaries.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220625215526_BlurUnreadSummaries")] diff --git a/API/Data/Migrations/20220625215526_BlurUnreadSummaries.cs b/Kavita.Database/Migrations/20220625215526_BlurUnreadSummaries.cs similarity index 94% rename from API/Data/Migrations/20220625215526_BlurUnreadSummaries.cs rename to Kavita.Database/Migrations/20220625215526_BlurUnreadSummaries.cs index 1da6e8d3e..463e50dba 100644 --- a/API/Data/Migrations/20220625215526_BlurUnreadSummaries.cs +++ b/Kavita.Database/Migrations/20220625215526_BlurUnreadSummaries.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class BlurUnreadSummaries : Migration { diff --git a/API/Data/Migrations/20220712161611_PromptForDownloadSizeUserOption.Designer.cs b/Kavita.Database/Migrations/20220712161611_PromptForDownloadSizeUserOption.Designer.cs similarity index 99% rename from API/Data/Migrations/20220712161611_PromptForDownloadSizeUserOption.Designer.cs rename to Kavita.Database/Migrations/20220712161611_PromptForDownloadSizeUserOption.Designer.cs index a2eb08e68..a83b64f20 100644 --- a/API/Data/Migrations/20220712161611_PromptForDownloadSizeUserOption.Designer.cs +++ b/Kavita.Database/Migrations/20220712161611_PromptForDownloadSizeUserOption.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220712161611_PromptForDownloadSizeUserOption")] diff --git a/API/Data/Migrations/20220712161611_PromptForDownloadSizeUserOption.cs b/Kavita.Database/Migrations/20220712161611_PromptForDownloadSizeUserOption.cs similarity index 95% rename from API/Data/Migrations/20220712161611_PromptForDownloadSizeUserOption.cs rename to Kavita.Database/Migrations/20220712161611_PromptForDownloadSizeUserOption.cs index 9bd994ed7..5eb9da3d5 100644 --- a/API/Data/Migrations/20220712161611_PromptForDownloadSizeUserOption.cs +++ b/Kavita.Database/Migrations/20220712161611_PromptForDownloadSizeUserOption.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class PromptForDownloadSizeUserOption : Migration { diff --git a/API/Data/Migrations/20220717145254_UserConfirmationLink.Designer.cs b/Kavita.Database/Migrations/20220717145254_UserConfirmationLink.Designer.cs similarity index 99% rename from API/Data/Migrations/20220717145254_UserConfirmationLink.Designer.cs rename to Kavita.Database/Migrations/20220717145254_UserConfirmationLink.Designer.cs index 14c2500a8..564df3cb4 100644 --- a/API/Data/Migrations/20220717145254_UserConfirmationLink.Designer.cs +++ b/Kavita.Database/Migrations/20220717145254_UserConfirmationLink.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220717145254_UserConfirmationLink")] diff --git a/API/Data/Migrations/20220717145254_UserConfirmationLink.cs b/Kavita.Database/Migrations/20220717145254_UserConfirmationLink.cs similarity index 94% rename from API/Data/Migrations/20220717145254_UserConfirmationLink.cs rename to Kavita.Database/Migrations/20220717145254_UserConfirmationLink.cs index 2ceba5b04..c48e4bfeb 100644 --- a/API/Data/Migrations/20220717145254_UserConfirmationLink.cs +++ b/Kavita.Database/Migrations/20220717145254_UserConfirmationLink.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class UserConfirmationLink : Migration { diff --git a/API/Data/Migrations/20220728193758_WantToReadList.Designer.cs b/Kavita.Database/Migrations/20220728193758_WantToReadList.Designer.cs similarity index 99% rename from API/Data/Migrations/20220728193758_WantToReadList.Designer.cs rename to Kavita.Database/Migrations/20220728193758_WantToReadList.Designer.cs index 989841071..68e592e52 100644 --- a/API/Data/Migrations/20220728193758_WantToReadList.Designer.cs +++ b/Kavita.Database/Migrations/20220728193758_WantToReadList.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220728193758_WantToReadList")] diff --git a/API/Data/Migrations/20220728193758_WantToReadList.cs b/Kavita.Database/Migrations/20220728193758_WantToReadList.cs similarity index 97% rename from API/Data/Migrations/20220728193758_WantToReadList.cs rename to Kavita.Database/Migrations/20220728193758_WantToReadList.cs index 6a3688380..b68a62995 100644 --- a/API/Data/Migrations/20220728193758_WantToReadList.cs +++ b/Kavita.Database/Migrations/20220728193758_WantToReadList.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class WantToReadList : Migration { diff --git a/API/Data/Migrations/20220802222910_BookmarkHasDate.Designer.cs b/Kavita.Database/Migrations/20220802222910_BookmarkHasDate.Designer.cs similarity index 99% rename from API/Data/Migrations/20220802222910_BookmarkHasDate.Designer.cs rename to Kavita.Database/Migrations/20220802222910_BookmarkHasDate.Designer.cs index 7ca5b6beb..141b4d1bb 100644 --- a/API/Data/Migrations/20220802222910_BookmarkHasDate.Designer.cs +++ b/Kavita.Database/Migrations/20220802222910_BookmarkHasDate.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220802222910_BookmarkHasDate")] diff --git a/API/Data/Migrations/20220802222910_BookmarkHasDate.cs b/Kavita.Database/Migrations/20220802222910_BookmarkHasDate.cs similarity index 96% rename from API/Data/Migrations/20220802222910_BookmarkHasDate.cs rename to Kavita.Database/Migrations/20220802222910_BookmarkHasDate.cs index eee40b647..2bb18387e 100644 --- a/API/Data/Migrations/20220802222910_BookmarkHasDate.cs +++ b/Kavita.Database/Migrations/20220802222910_BookmarkHasDate.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class BookmarkHasDate : Migration { diff --git a/API/Data/Migrations/20220814134725_MangaFileCreatedDate.Designer.cs b/Kavita.Database/Migrations/20220814134725_MangaFileCreatedDate.Designer.cs similarity index 99% rename from API/Data/Migrations/20220814134725_MangaFileCreatedDate.Designer.cs rename to Kavita.Database/Migrations/20220814134725_MangaFileCreatedDate.Designer.cs index 747bfecea..72255b076 100644 --- a/API/Data/Migrations/20220814134725_MangaFileCreatedDate.Designer.cs +++ b/Kavita.Database/Migrations/20220814134725_MangaFileCreatedDate.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220814134725_MangaFileCreatedDate")] diff --git a/API/Data/Migrations/20220814134725_MangaFileCreatedDate.cs b/Kavita.Database/Migrations/20220814134725_MangaFileCreatedDate.cs similarity index 95% rename from API/Data/Migrations/20220814134725_MangaFileCreatedDate.cs rename to Kavita.Database/Migrations/20220814134725_MangaFileCreatedDate.cs index 09aa49746..90d46cc2d 100644 --- a/API/Data/Migrations/20220814134725_MangaFileCreatedDate.cs +++ b/Kavita.Database/Migrations/20220814134725_MangaFileCreatedDate.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class MangaFileCreatedDate : Migration { diff --git a/API/Data/Migrations/20220817173731_SeriesFolder.Designer.cs b/Kavita.Database/Migrations/20220817173731_SeriesFolder.Designer.cs similarity index 99% rename from API/Data/Migrations/20220817173731_SeriesFolder.Designer.cs rename to Kavita.Database/Migrations/20220817173731_SeriesFolder.Designer.cs index 96fed7004..08341edb2 100644 --- a/API/Data/Migrations/20220817173731_SeriesFolder.Designer.cs +++ b/Kavita.Database/Migrations/20220817173731_SeriesFolder.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220817173731_SeriesFolder")] diff --git a/API/Data/Migrations/20220817173731_SeriesFolder.cs b/Kavita.Database/Migrations/20220817173731_SeriesFolder.cs similarity index 96% rename from API/Data/Migrations/20220817173731_SeriesFolder.cs rename to Kavita.Database/Migrations/20220817173731_SeriesFolder.cs index 33373c0c4..f1d58f4a7 100644 --- a/API/Data/Migrations/20220817173731_SeriesFolder.cs +++ b/Kavita.Database/Migrations/20220817173731_SeriesFolder.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SeriesFolder : Migration { diff --git a/API/Data/Migrations/20220819223212_NormalizedLocalizedName.Designer.cs b/Kavita.Database/Migrations/20220819223212_NormalizedLocalizedName.Designer.cs similarity index 99% rename from API/Data/Migrations/20220819223212_NormalizedLocalizedName.Designer.cs rename to Kavita.Database/Migrations/20220819223212_NormalizedLocalizedName.Designer.cs index 41bf29e94..7ab149613 100644 --- a/API/Data/Migrations/20220819223212_NormalizedLocalizedName.Designer.cs +++ b/Kavita.Database/Migrations/20220819223212_NormalizedLocalizedName.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220819223212_NormalizedLocalizedName")] diff --git a/API/Data/Migrations/20220819223212_NormalizedLocalizedName.cs b/Kavita.Database/Migrations/20220819223212_NormalizedLocalizedName.cs similarity index 94% rename from API/Data/Migrations/20220819223212_NormalizedLocalizedName.cs rename to Kavita.Database/Migrations/20220819223212_NormalizedLocalizedName.cs index 600a3a6b2..12c52b93e 100644 --- a/API/Data/Migrations/20220819223212_NormalizedLocalizedName.cs +++ b/Kavita.Database/Migrations/20220819223212_NormalizedLocalizedName.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class NormalizedLocalizedName : Migration { diff --git a/API/Data/Migrations/20220921023455_DeviceSupport.Designer.cs b/Kavita.Database/Migrations/20220921023455_DeviceSupport.Designer.cs similarity index 99% rename from API/Data/Migrations/20220921023455_DeviceSupport.Designer.cs rename to Kavita.Database/Migrations/20220921023455_DeviceSupport.Designer.cs index dbf4a0af6..95ccb38b6 100644 --- a/API/Data/Migrations/20220921023455_DeviceSupport.Designer.cs +++ b/Kavita.Database/Migrations/20220921023455_DeviceSupport.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220921023455_DeviceSupport")] diff --git a/API/Data/Migrations/20220921023455_DeviceSupport.cs b/Kavita.Database/Migrations/20220921023455_DeviceSupport.cs similarity index 98% rename from API/Data/Migrations/20220921023455_DeviceSupport.cs rename to Kavita.Database/Migrations/20220921023455_DeviceSupport.cs index 7723daa41..ae8c3d220 100644 --- a/API/Data/Migrations/20220921023455_DeviceSupport.cs +++ b/Kavita.Database/Migrations/20220921023455_DeviceSupport.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class DeviceSupport : Migration { diff --git a/API/Data/Migrations/20220926145902_AddNoTransitions.Designer.cs b/Kavita.Database/Migrations/20220926145902_AddNoTransitions.Designer.cs similarity index 99% rename from API/Data/Migrations/20220926145902_AddNoTransitions.Designer.cs rename to Kavita.Database/Migrations/20220926145902_AddNoTransitions.Designer.cs index af7f8bd07..d811a7b62 100644 --- a/API/Data/Migrations/20220926145902_AddNoTransitions.Designer.cs +++ b/Kavita.Database/Migrations/20220926145902_AddNoTransitions.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20220926145902_AddNoTransitions")] diff --git a/API/Data/Migrations/20220926145902_AddNoTransitions.cs b/Kavita.Database/Migrations/20220926145902_AddNoTransitions.cs similarity index 94% rename from API/Data/Migrations/20220926145902_AddNoTransitions.cs rename to Kavita.Database/Migrations/20220926145902_AddNoTransitions.cs index fcef3979a..a14bf84c9 100644 --- a/API/Data/Migrations/20220926145902_AddNoTransitions.cs +++ b/Kavita.Database/Migrations/20220926145902_AddNoTransitions.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class AddNoTransitions : Migration { diff --git a/API/Data/Migrations/20221006013956_ReleaseYearOnSeriesEdit.Designer.cs b/Kavita.Database/Migrations/20221006013956_ReleaseYearOnSeriesEdit.Designer.cs similarity index 99% rename from API/Data/Migrations/20221006013956_ReleaseYearOnSeriesEdit.Designer.cs rename to Kavita.Database/Migrations/20221006013956_ReleaseYearOnSeriesEdit.Designer.cs index fcc054561..554c699f8 100644 --- a/API/Data/Migrations/20221006013956_ReleaseYearOnSeriesEdit.Designer.cs +++ b/Kavita.Database/Migrations/20221006013956_ReleaseYearOnSeriesEdit.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20221006013956_ReleaseYearOnSeriesEdit")] diff --git a/API/Data/Migrations/20221006013956_ReleaseYearOnSeriesEdit.cs b/Kavita.Database/Migrations/20221006013956_ReleaseYearOnSeriesEdit.cs similarity index 94% rename from API/Data/Migrations/20221006013956_ReleaseYearOnSeriesEdit.cs rename to Kavita.Database/Migrations/20221006013956_ReleaseYearOnSeriesEdit.cs index e96557e4e..85b89b11e 100644 --- a/API/Data/Migrations/20221006013956_ReleaseYearOnSeriesEdit.cs +++ b/Kavita.Database/Migrations/20221006013956_ReleaseYearOnSeriesEdit.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ReleaseYearOnSeriesEdit : Migration { diff --git a/API/Data/Migrations/20221009172653_ReadingListAgeRating.Designer.cs b/Kavita.Database/Migrations/20221009172653_ReadingListAgeRating.Designer.cs similarity index 99% rename from API/Data/Migrations/20221009172653_ReadingListAgeRating.Designer.cs rename to Kavita.Database/Migrations/20221009172653_ReadingListAgeRating.Designer.cs index f93e7a58d..f297b18a0 100644 --- a/API/Data/Migrations/20221009172653_ReadingListAgeRating.Designer.cs +++ b/Kavita.Database/Migrations/20221009172653_ReadingListAgeRating.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20221009172653_ReadingListAgeRating")] diff --git a/API/Data/Migrations/20221009172653_ReadingListAgeRating.cs b/Kavita.Database/Migrations/20221009172653_ReadingListAgeRating.cs similarity index 94% rename from API/Data/Migrations/20221009172653_ReadingListAgeRating.cs rename to Kavita.Database/Migrations/20221009172653_ReadingListAgeRating.cs index dfc69a9cf..9894ae82b 100644 --- a/API/Data/Migrations/20221009172653_ReadingListAgeRating.cs +++ b/Kavita.Database/Migrations/20221009172653_ReadingListAgeRating.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ReadingListAgeRating : Migration { diff --git a/API/Data/Migrations/20221009211237_UserAgeRating.Designer.cs b/Kavita.Database/Migrations/20221009211237_UserAgeRating.Designer.cs similarity index 99% rename from API/Data/Migrations/20221009211237_UserAgeRating.Designer.cs rename to Kavita.Database/Migrations/20221009211237_UserAgeRating.Designer.cs index 1a9e9fade..77e74d838 100644 --- a/API/Data/Migrations/20221009211237_UserAgeRating.Designer.cs +++ b/Kavita.Database/Migrations/20221009211237_UserAgeRating.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20221009211237_UserAgeRating")] diff --git a/API/Data/Migrations/20221009211237_UserAgeRating.cs b/Kavita.Database/Migrations/20221009211237_UserAgeRating.cs similarity index 89% rename from API/Data/Migrations/20221009211237_UserAgeRating.cs rename to Kavita.Database/Migrations/20221009211237_UserAgeRating.cs index a619255ef..12436af4a 100644 --- a/API/Data/Migrations/20221009211237_UserAgeRating.cs +++ b/Kavita.Database/Migrations/20221009211237_UserAgeRating.cs @@ -1,9 +1,9 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class UserAgeRating : Migration { diff --git a/API/Data/Migrations/20221017131711_IncludeUnknowns.Designer.cs b/Kavita.Database/Migrations/20221017131711_IncludeUnknowns.Designer.cs similarity index 99% rename from API/Data/Migrations/20221017131711_IncludeUnknowns.Designer.cs rename to Kavita.Database/Migrations/20221017131711_IncludeUnknowns.Designer.cs index 9ad6b3542..f728792bc 100644 --- a/API/Data/Migrations/20221017131711_IncludeUnknowns.Designer.cs +++ b/Kavita.Database/Migrations/20221017131711_IncludeUnknowns.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20221017131711_IncludeUnknowns")] diff --git a/API/Data/Migrations/20221017131711_IncludeUnknowns.cs b/Kavita.Database/Migrations/20221017131711_IncludeUnknowns.cs similarity index 94% rename from API/Data/Migrations/20221017131711_IncludeUnknowns.cs rename to Kavita.Database/Migrations/20221017131711_IncludeUnknowns.cs index 34c0dfd9e..1ea104222 100644 --- a/API/Data/Migrations/20221017131711_IncludeUnknowns.cs +++ b/Kavita.Database/Migrations/20221017131711_IncludeUnknowns.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class IncludeUnknowns : Migration { diff --git a/API/Data/Migrations/20221115021908_SeriesRelationChange.Designer.cs b/Kavita.Database/Migrations/20221115021908_SeriesRelationChange.Designer.cs similarity index 99% rename from API/Data/Migrations/20221115021908_SeriesRelationChange.Designer.cs rename to Kavita.Database/Migrations/20221115021908_SeriesRelationChange.Designer.cs index d9a964ad0..b07980071 100644 --- a/API/Data/Migrations/20221115021908_SeriesRelationChange.Designer.cs +++ b/Kavita.Database/Migrations/20221115021908_SeriesRelationChange.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20221115021908_SeriesRelationChange")] diff --git a/API/Data/Migrations/20221115021908_SeriesRelationChange.cs b/Kavita.Database/Migrations/20221115021908_SeriesRelationChange.cs similarity index 98% rename from API/Data/Migrations/20221115021908_SeriesRelationChange.cs rename to Kavita.Database/Migrations/20221115021908_SeriesRelationChange.cs index 83c3fdc60..e73e1b4da 100644 --- a/API/Data/Migrations/20221115021908_SeriesRelationChange.cs +++ b/Kavita.Database/Migrations/20221115021908_SeriesRelationChange.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SeriesRelationChange : Migration { diff --git a/API/Data/Migrations/20221118131123_ExtendedLibrarySettings.Designer.cs b/Kavita.Database/Migrations/20221118131123_ExtendedLibrarySettings.Designer.cs similarity index 99% rename from API/Data/Migrations/20221118131123_ExtendedLibrarySettings.Designer.cs rename to Kavita.Database/Migrations/20221118131123_ExtendedLibrarySettings.Designer.cs index e79dddcbc..b83fea2cf 100644 --- a/API/Data/Migrations/20221118131123_ExtendedLibrarySettings.Designer.cs +++ b/Kavita.Database/Migrations/20221118131123_ExtendedLibrarySettings.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20221118131123_ExtendedLibrarySettings")] diff --git a/API/Data/Migrations/20221118131123_ExtendedLibrarySettings.cs b/Kavita.Database/Migrations/20221118131123_ExtendedLibrarySettings.cs similarity index 97% rename from API/Data/Migrations/20221118131123_ExtendedLibrarySettings.cs rename to Kavita.Database/Migrations/20221118131123_ExtendedLibrarySettings.cs index 1c05b6b5b..0b291ed62 100644 --- a/API/Data/Migrations/20221118131123_ExtendedLibrarySettings.cs +++ b/Kavita.Database/Migrations/20221118131123_ExtendedLibrarySettings.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ExtendedLibrarySettings : Migration { diff --git a/API/Data/Migrations/20221126133824_FileLengthAndExtension.Designer.cs b/Kavita.Database/Migrations/20221126133824_FileLengthAndExtension.Designer.cs similarity index 99% rename from API/Data/Migrations/20221126133824_FileLengthAndExtension.Designer.cs rename to Kavita.Database/Migrations/20221126133824_FileLengthAndExtension.Designer.cs index 17cfe499d..6762bc0e9 100644 --- a/API/Data/Migrations/20221126133824_FileLengthAndExtension.Designer.cs +++ b/Kavita.Database/Migrations/20221126133824_FileLengthAndExtension.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20221126133824_FileLengthAndExtension")] diff --git a/API/Data/Migrations/20221126133824_FileLengthAndExtension.cs b/Kavita.Database/Migrations/20221126133824_FileLengthAndExtension.cs similarity index 96% rename from API/Data/Migrations/20221126133824_FileLengthAndExtension.cs rename to Kavita.Database/Migrations/20221126133824_FileLengthAndExtension.cs index d07deaf89..92c45bd00 100644 --- a/API/Data/Migrations/20221126133824_FileLengthAndExtension.cs +++ b/Kavita.Database/Migrations/20221126133824_FileLengthAndExtension.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class FileLengthAndExtension : Migration { diff --git a/API/Data/Migrations/20221128230726_UserProgressLibraryId.Designer.cs b/Kavita.Database/Migrations/20221128230726_UserProgressLibraryId.Designer.cs similarity index 99% rename from API/Data/Migrations/20221128230726_UserProgressLibraryId.Designer.cs rename to Kavita.Database/Migrations/20221128230726_UserProgressLibraryId.Designer.cs index 067f7d486..1957b4c83 100644 --- a/API/Data/Migrations/20221128230726_UserProgressLibraryId.Designer.cs +++ b/Kavita.Database/Migrations/20221128230726_UserProgressLibraryId.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20221128230726_UserProgressLibraryId")] diff --git a/API/Data/Migrations/20221128230726_UserProgressLibraryId.cs b/Kavita.Database/Migrations/20221128230726_UserProgressLibraryId.cs similarity index 94% rename from API/Data/Migrations/20221128230726_UserProgressLibraryId.cs rename to Kavita.Database/Migrations/20221128230726_UserProgressLibraryId.cs index 383507825..adb5daad6 100644 --- a/API/Data/Migrations/20221128230726_UserProgressLibraryId.cs +++ b/Kavita.Database/Migrations/20221128230726_UserProgressLibraryId.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class UserProgressLibraryId : Migration { diff --git a/API/Data/Migrations/20221212215914_EmulateBookPref.Designer.cs b/Kavita.Database/Migrations/20221212215914_EmulateBookPref.Designer.cs similarity index 99% rename from API/Data/Migrations/20221212215914_EmulateBookPref.Designer.cs rename to Kavita.Database/Migrations/20221212215914_EmulateBookPref.Designer.cs index 431307ba2..fab4c0d35 100644 --- a/API/Data/Migrations/20221212215914_EmulateBookPref.Designer.cs +++ b/Kavita.Database/Migrations/20221212215914_EmulateBookPref.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20221212215914_EmulateBookPref")] diff --git a/API/Data/Migrations/20221212215914_EmulateBookPref.cs b/Kavita.Database/Migrations/20221212215914_EmulateBookPref.cs similarity index 94% rename from API/Data/Migrations/20221212215914_EmulateBookPref.cs rename to Kavita.Database/Migrations/20221212215914_EmulateBookPref.cs index d2883ba0c..baeea39d5 100644 --- a/API/Data/Migrations/20221212215914_EmulateBookPref.cs +++ b/Kavita.Database/Migrations/20221212215914_EmulateBookPref.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class EmulateBookPref : Migration { diff --git a/API/Data/Migrations/20230111014852_YearlyStats.Designer.cs b/Kavita.Database/Migrations/20230111014852_YearlyStats.Designer.cs similarity index 99% rename from API/Data/Migrations/20230111014852_YearlyStats.Designer.cs rename to Kavita.Database/Migrations/20230111014852_YearlyStats.Designer.cs index 2a34ad07b..a4a0d597f 100644 --- a/API/Data/Migrations/20230111014852_YearlyStats.Designer.cs +++ b/Kavita.Database/Migrations/20230111014852_YearlyStats.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230111014852_YearlyStats")] diff --git a/API/Data/Migrations/20230111014852_YearlyStats.cs b/Kavita.Database/Migrations/20230111014852_YearlyStats.cs similarity index 97% rename from API/Data/Migrations/20230111014852_YearlyStats.cs rename to Kavita.Database/Migrations/20230111014852_YearlyStats.cs index c2ec76e3b..58ef17de6 100644 --- a/API/Data/Migrations/20230111014852_YearlyStats.cs +++ b/Kavita.Database/Migrations/20230111014852_YearlyStats.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class YearlyStats : Migration { diff --git a/API/Data/Migrations/20230129210741_SwipeToPaginatePref.Designer.cs b/Kavita.Database/Migrations/20230129210741_SwipeToPaginatePref.Designer.cs similarity index 99% rename from API/Data/Migrations/20230129210741_SwipeToPaginatePref.Designer.cs rename to Kavita.Database/Migrations/20230129210741_SwipeToPaginatePref.Designer.cs index ea948ab31..5ebcf503d 100644 --- a/API/Data/Migrations/20230129210741_SwipeToPaginatePref.Designer.cs +++ b/Kavita.Database/Migrations/20230129210741_SwipeToPaginatePref.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230129210741_SwipeToPaginatePref")] diff --git a/API/Data/Migrations/20230129210741_SwipeToPaginatePref.cs b/Kavita.Database/Migrations/20230129210741_SwipeToPaginatePref.cs similarity index 94% rename from API/Data/Migrations/20230129210741_SwipeToPaginatePref.cs rename to Kavita.Database/Migrations/20230129210741_SwipeToPaginatePref.cs index 0f99c4c26..e8b7e7d90 100644 --- a/API/Data/Migrations/20230129210741_SwipeToPaginatePref.cs +++ b/Kavita.Database/Migrations/20230129210741_SwipeToPaginatePref.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class SwipeToPaginatePref : Migration { diff --git a/API/Data/Migrations/20230130210252_AutoCollections.Designer.cs b/Kavita.Database/Migrations/20230130210252_AutoCollections.Designer.cs similarity index 99% rename from API/Data/Migrations/20230130210252_AutoCollections.Designer.cs rename to Kavita.Database/Migrations/20230130210252_AutoCollections.Designer.cs index 6406e7335..5bc4dc44e 100644 --- a/API/Data/Migrations/20230130210252_AutoCollections.Designer.cs +++ b/Kavita.Database/Migrations/20230130210252_AutoCollections.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230130210252_AutoCollections")] diff --git a/API/Data/Migrations/20230130210252_AutoCollections.cs b/Kavita.Database/Migrations/20230130210252_AutoCollections.cs similarity index 96% rename from API/Data/Migrations/20230130210252_AutoCollections.cs rename to Kavita.Database/Migrations/20230130210252_AutoCollections.cs index 86d2dd3c1..707647539 100644 --- a/API/Data/Migrations/20230130210252_AutoCollections.cs +++ b/Kavita.Database/Migrations/20230130210252_AutoCollections.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class AutoCollections : Migration { diff --git a/API/Data/Migrations/20230202182602_ReadingListFields.Designer.cs b/Kavita.Database/Migrations/20230202182602_ReadingListFields.Designer.cs similarity index 99% rename from API/Data/Migrations/20230202182602_ReadingListFields.Designer.cs rename to Kavita.Database/Migrations/20230202182602_ReadingListFields.Designer.cs index 9ccab0a26..e492754b1 100644 --- a/API/Data/Migrations/20230202182602_ReadingListFields.Designer.cs +++ b/Kavita.Database/Migrations/20230202182602_ReadingListFields.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230202182602_ReadingListFields")] diff --git a/API/Data/Migrations/20230202182602_ReadingListFields.cs b/Kavita.Database/Migrations/20230202182602_ReadingListFields.cs similarity index 98% rename from API/Data/Migrations/20230202182602_ReadingListFields.cs rename to Kavita.Database/Migrations/20230202182602_ReadingListFields.cs index b8cc32bd2..26e4efbab 100644 --- a/API/Data/Migrations/20230202182602_ReadingListFields.cs +++ b/Kavita.Database/Migrations/20230202182602_ReadingListFields.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class ReadingListFields : Migration { diff --git a/API/Data/Migrations/20230203112022_RemoveExternalFromTagAndGenre.Designer.cs b/Kavita.Database/Migrations/20230203112022_RemoveExternalFromTagAndGenre.Designer.cs similarity index 99% rename from API/Data/Migrations/20230203112022_RemoveExternalFromTagAndGenre.Designer.cs rename to Kavita.Database/Migrations/20230203112022_RemoveExternalFromTagAndGenre.Designer.cs index 008e9690f..cf041be6b 100644 --- a/API/Data/Migrations/20230203112022_RemoveExternalFromTagAndGenre.Designer.cs +++ b/Kavita.Database/Migrations/20230203112022_RemoveExternalFromTagAndGenre.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230203112022_RemoveExternalFromTagAndGenre")] diff --git a/API/Data/Migrations/20230203112022_RemoveExternalFromTagAndGenre.cs b/Kavita.Database/Migrations/20230203112022_RemoveExternalFromTagAndGenre.cs similarity index 98% rename from API/Data/Migrations/20230203112022_RemoveExternalFromTagAndGenre.cs rename to Kavita.Database/Migrations/20230203112022_RemoveExternalFromTagAndGenre.cs index 44216e4db..96f2a8a1f 100644 --- a/API/Data/Migrations/20230203112022_RemoveExternalFromTagAndGenre.cs +++ b/Kavita.Database/Migrations/20230203112022_RemoveExternalFromTagAndGenre.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class RemoveExternalFromTagAndGenre : Migration { diff --git a/API/Data/Migrations/20230210153842_UtcTimes.Designer.cs b/Kavita.Database/Migrations/20230210153842_UtcTimes.Designer.cs similarity index 99% rename from API/Data/Migrations/20230210153842_UtcTimes.Designer.cs rename to Kavita.Database/Migrations/20230210153842_UtcTimes.Designer.cs index ff9394649..7f2a71b8d 100644 --- a/API/Data/Migrations/20230210153842_UtcTimes.Designer.cs +++ b/Kavita.Database/Migrations/20230210153842_UtcTimes.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230210153842_UtcTimes")] diff --git a/API/Data/Migrations/20230210153842_UtcTimes.cs b/Kavita.Database/Migrations/20230210153842_UtcTimes.cs similarity index 99% rename from API/Data/Migrations/20230210153842_UtcTimes.cs rename to Kavita.Database/Migrations/20230210153842_UtcTimes.cs index 9354cded7..6eaf4906c 100644 --- a/API/Data/Migrations/20230210153842_UtcTimes.cs +++ b/Kavita.Database/Migrations/20230210153842_UtcTimes.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class UtcTimes : Migration { diff --git a/API/Data/Migrations/20230220203128_CollapseSeriesRelationships.Designer.cs b/Kavita.Database/Migrations/20230220203128_CollapseSeriesRelationships.Designer.cs similarity index 99% rename from API/Data/Migrations/20230220203128_CollapseSeriesRelationships.Designer.cs rename to Kavita.Database/Migrations/20230220203128_CollapseSeriesRelationships.Designer.cs index 521ac509f..797767a7b 100644 --- a/API/Data/Migrations/20230220203128_CollapseSeriesRelationships.Designer.cs +++ b/Kavita.Database/Migrations/20230220203128_CollapseSeriesRelationships.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230220203128_CollapseSeriesRelationships")] diff --git a/API/Data/Migrations/20230220203128_CollapseSeriesRelationships.cs b/Kavita.Database/Migrations/20230220203128_CollapseSeriesRelationships.cs similarity index 94% rename from API/Data/Migrations/20230220203128_CollapseSeriesRelationships.cs rename to Kavita.Database/Migrations/20230220203128_CollapseSeriesRelationships.cs index 2e06924cd..ee190814f 100644 --- a/API/Data/Migrations/20230220203128_CollapseSeriesRelationships.cs +++ b/Kavita.Database/Migrations/20230220203128_CollapseSeriesRelationships.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class CollapseSeriesRelationships : Migration { diff --git a/API/Data/Migrations/20230304202540_BookWritingStylePref.Designer.cs b/Kavita.Database/Migrations/20230304202540_BookWritingStylePref.Designer.cs similarity index 99% rename from API/Data/Migrations/20230304202540_BookWritingStylePref.Designer.cs rename to Kavita.Database/Migrations/20230304202540_BookWritingStylePref.Designer.cs index 37cc255ae..6d2353347 100644 --- a/API/Data/Migrations/20230304202540_BookWritingStylePref.Designer.cs +++ b/Kavita.Database/Migrations/20230304202540_BookWritingStylePref.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230304202540_BookWritingStylePref")] diff --git a/API/Data/Migrations/20230304202540_BookWritingStylePref.cs b/Kavita.Database/Migrations/20230304202540_BookWritingStylePref.cs similarity index 94% rename from API/Data/Migrations/20230304202540_BookWritingStylePref.cs rename to Kavita.Database/Migrations/20230304202540_BookWritingStylePref.cs index fd6703060..601bce1cb 100644 --- a/API/Data/Migrations/20230304202540_BookWritingStylePref.cs +++ b/Kavita.Database/Migrations/20230304202540_BookWritingStylePref.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { public partial class BookWritingStylePref : Migration { diff --git a/API/Data/Migrations/20230310142630_MoveCollapseSeriesToUserPref.Designer.cs b/Kavita.Database/Migrations/20230310142630_MoveCollapseSeriesToUserPref.Designer.cs similarity index 99% rename from API/Data/Migrations/20230310142630_MoveCollapseSeriesToUserPref.Designer.cs rename to Kavita.Database/Migrations/20230310142630_MoveCollapseSeriesToUserPref.Designer.cs index 2edee6323..e7083fee9 100644 --- a/API/Data/Migrations/20230310142630_MoveCollapseSeriesToUserPref.Designer.cs +++ b/Kavita.Database/Migrations/20230310142630_MoveCollapseSeriesToUserPref.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230310142630_MoveCollapseSeriesToUserPref")] diff --git a/API/Data/Migrations/20230310142630_MoveCollapseSeriesToUserPref.cs b/Kavita.Database/Migrations/20230310142630_MoveCollapseSeriesToUserPref.cs similarity index 95% rename from API/Data/Migrations/20230310142630_MoveCollapseSeriesToUserPref.cs rename to Kavita.Database/Migrations/20230310142630_MoveCollapseSeriesToUserPref.cs index db5920d0a..a704587d5 100644 --- a/API/Data/Migrations/20230310142630_MoveCollapseSeriesToUserPref.cs +++ b/Kavita.Database/Migrations/20230310142630_MoveCollapseSeriesToUserPref.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class MoveCollapseSeriesToUserPref : Migration diff --git a/API/Data/Migrations/20230313125914_ReadingListDateRange.Designer.cs b/Kavita.Database/Migrations/20230313125914_ReadingListDateRange.Designer.cs similarity index 99% rename from API/Data/Migrations/20230313125914_ReadingListDateRange.Designer.cs rename to Kavita.Database/Migrations/20230313125914_ReadingListDateRange.Designer.cs index 3500a3080..98a916137 100644 --- a/API/Data/Migrations/20230313125914_ReadingListDateRange.Designer.cs +++ b/Kavita.Database/Migrations/20230313125914_ReadingListDateRange.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230313125914_ReadingListDateRange")] diff --git a/API/Data/Migrations/20230313125914_ReadingListDateRange.cs b/Kavita.Database/Migrations/20230313125914_ReadingListDateRange.cs similarity index 98% rename from API/Data/Migrations/20230313125914_ReadingListDateRange.cs rename to Kavita.Database/Migrations/20230313125914_ReadingListDateRange.cs index e4de75aa2..6ecd2b68c 100644 --- a/API/Data/Migrations/20230313125914_ReadingListDateRange.cs +++ b/Kavita.Database/Migrations/20230313125914_ReadingListDateRange.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ReadingListDateRange : Migration diff --git a/API/Data/Migrations/20230316123908_SecurityEvent.Designer.cs b/Kavita.Database/Migrations/20230316123908_SecurityEvent.Designer.cs similarity index 99% rename from API/Data/Migrations/20230316123908_SecurityEvent.Designer.cs rename to Kavita.Database/Migrations/20230316123908_SecurityEvent.Designer.cs index e0c1b3bfb..539ada05b 100644 --- a/API/Data/Migrations/20230316123908_SecurityEvent.Designer.cs +++ b/Kavita.Database/Migrations/20230316123908_SecurityEvent.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230316123908_SecurityEvent")] diff --git a/API/Data/Migrations/20230316123908_SecurityEvent.cs b/Kavita.Database/Migrations/20230316123908_SecurityEvent.cs similarity index 97% rename from API/Data/Migrations/20230316123908_SecurityEvent.cs rename to Kavita.Database/Migrations/20230316123908_SecurityEvent.cs index ec4eab520..477121fbe 100644 --- a/API/Data/Migrations/20230316123908_SecurityEvent.cs +++ b/Kavita.Database/Migrations/20230316123908_SecurityEvent.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class SecurityEvent : Migration diff --git a/API/Data/Migrations/20230316233133_RemoveSecurityEvent.Designer.cs b/Kavita.Database/Migrations/20230316233133_RemoveSecurityEvent.Designer.cs similarity index 99% rename from API/Data/Migrations/20230316233133_RemoveSecurityEvent.Designer.cs rename to Kavita.Database/Migrations/20230316233133_RemoveSecurityEvent.Designer.cs index f6da45449..1e051b6e8 100644 --- a/API/Data/Migrations/20230316233133_RemoveSecurityEvent.Designer.cs +++ b/Kavita.Database/Migrations/20230316233133_RemoveSecurityEvent.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230316233133_RemoveSecurityEvent")] diff --git a/API/Data/Migrations/20230316233133_RemoveSecurityEvent.cs b/Kavita.Database/Migrations/20230316233133_RemoveSecurityEvent.cs similarity index 97% rename from API/Data/Migrations/20230316233133_RemoveSecurityEvent.cs rename to Kavita.Database/Migrations/20230316233133_RemoveSecurityEvent.cs index d0d4c5c73..23e4078a7 100644 --- a/API/Data/Migrations/20230316233133_RemoveSecurityEvent.cs +++ b/Kavita.Database/Migrations/20230316233133_RemoveSecurityEvent.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class RemoveSecurityEvent : Migration diff --git a/API/Data/Migrations/20230415123449_ManageReadingListOnLibrary.Designer.cs b/Kavita.Database/Migrations/20230415123449_ManageReadingListOnLibrary.Designer.cs similarity index 99% rename from API/Data/Migrations/20230415123449_ManageReadingListOnLibrary.Designer.cs rename to Kavita.Database/Migrations/20230415123449_ManageReadingListOnLibrary.Designer.cs index 3ef88948b..cd6bc19ea 100644 --- a/API/Data/Migrations/20230415123449_ManageReadingListOnLibrary.Designer.cs +++ b/Kavita.Database/Migrations/20230415123449_ManageReadingListOnLibrary.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230415123449_ManageReadingListOnLibrary")] diff --git a/API/Data/Migrations/20230415123449_ManageReadingListOnLibrary.cs b/Kavita.Database/Migrations/20230415123449_ManageReadingListOnLibrary.cs similarity index 95% rename from API/Data/Migrations/20230415123449_ManageReadingListOnLibrary.cs rename to Kavita.Database/Migrations/20230415123449_ManageReadingListOnLibrary.cs index 3c57d3de3..40294f07b 100644 --- a/API/Data/Migrations/20230415123449_ManageReadingListOnLibrary.cs +++ b/Kavita.Database/Migrations/20230415123449_ManageReadingListOnLibrary.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ManageReadingListOnLibrary : Migration diff --git a/API/Data/Migrations/20230505124430_MediaError.Designer.cs b/Kavita.Database/Migrations/20230505124430_MediaError.Designer.cs similarity index 99% rename from API/Data/Migrations/20230505124430_MediaError.Designer.cs rename to Kavita.Database/Migrations/20230505124430_MediaError.Designer.cs index f3e770fa1..f9faaccff 100644 --- a/API/Data/Migrations/20230505124430_MediaError.Designer.cs +++ b/Kavita.Database/Migrations/20230505124430_MediaError.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230505124430_MediaError")] diff --git a/API/Data/Migrations/20230505124430_MediaError.cs b/Kavita.Database/Migrations/20230505124430_MediaError.cs similarity index 97% rename from API/Data/Migrations/20230505124430_MediaError.cs rename to Kavita.Database/Migrations/20230505124430_MediaError.cs index 9bf69d3a2..7b5b04fcc 100644 --- a/API/Data/Migrations/20230505124430_MediaError.cs +++ b/Kavita.Database/Migrations/20230505124430_MediaError.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class MediaError : Migration diff --git a/API/Data/Migrations/20230511165427_WebLinksForChapter.Designer.cs b/Kavita.Database/Migrations/20230511165427_WebLinksForChapter.Designer.cs similarity index 99% rename from API/Data/Migrations/20230511165427_WebLinksForChapter.Designer.cs rename to Kavita.Database/Migrations/20230511165427_WebLinksForChapter.Designer.cs index 0dfd240c1..d64c5a8c2 100644 --- a/API/Data/Migrations/20230511165427_WebLinksForChapter.Designer.cs +++ b/Kavita.Database/Migrations/20230511165427_WebLinksForChapter.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230511165427_WebLinksForChapter")] diff --git a/API/Data/Migrations/20230511165427_WebLinksForChapter.cs b/Kavita.Database/Migrations/20230511165427_WebLinksForChapter.cs similarity index 95% rename from API/Data/Migrations/20230511165427_WebLinksForChapter.cs rename to Kavita.Database/Migrations/20230511165427_WebLinksForChapter.cs index 837117072..7438bb342 100644 --- a/API/Data/Migrations/20230511165427_WebLinksForChapter.cs +++ b/Kavita.Database/Migrations/20230511165427_WebLinksForChapter.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class WebLinksForChapter : Migration diff --git a/API/Data/Migrations/20230511183339_WebLinksForSeries.Designer.cs b/Kavita.Database/Migrations/20230511183339_WebLinksForSeries.Designer.cs similarity index 99% rename from API/Data/Migrations/20230511183339_WebLinksForSeries.Designer.cs rename to Kavita.Database/Migrations/20230511183339_WebLinksForSeries.Designer.cs index 5c6250d34..0d29e738a 100644 --- a/API/Data/Migrations/20230511183339_WebLinksForSeries.Designer.cs +++ b/Kavita.Database/Migrations/20230511183339_WebLinksForSeries.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230511183339_WebLinksForSeries")] diff --git a/API/Data/Migrations/20230511183339_WebLinksForSeries.cs b/Kavita.Database/Migrations/20230511183339_WebLinksForSeries.cs similarity index 95% rename from API/Data/Migrations/20230511183339_WebLinksForSeries.cs rename to Kavita.Database/Migrations/20230511183339_WebLinksForSeries.cs index 01117d2ad..b979adb11 100644 --- a/API/Data/Migrations/20230511183339_WebLinksForSeries.cs +++ b/Kavita.Database/Migrations/20230511183339_WebLinksForSeries.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class WebLinksForSeries : Migration diff --git a/API/Data/Migrations/20230512004545_ChapterISBN.Designer.cs b/Kavita.Database/Migrations/20230512004545_ChapterISBN.Designer.cs similarity index 99% rename from API/Data/Migrations/20230512004545_ChapterISBN.Designer.cs rename to Kavita.Database/Migrations/20230512004545_ChapterISBN.Designer.cs index 8354a7c30..3f2652709 100644 --- a/API/Data/Migrations/20230512004545_ChapterISBN.Designer.cs +++ b/Kavita.Database/Migrations/20230512004545_ChapterISBN.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230512004545_ChapterISBN")] diff --git a/API/Data/Migrations/20230512004545_ChapterISBN.cs b/Kavita.Database/Migrations/20230512004545_ChapterISBN.cs similarity index 95% rename from API/Data/Migrations/20230512004545_ChapterISBN.cs rename to Kavita.Database/Migrations/20230512004545_ChapterISBN.cs index b5d9ea84f..bcd26e1d0 100644 --- a/API/Data/Migrations/20230512004545_ChapterISBN.cs +++ b/Kavita.Database/Migrations/20230512004545_ChapterISBN.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ChapterISBN : Migration diff --git a/API/Data/Migrations/20230527215722_LicenseAndScrobble.Designer.cs b/Kavita.Database/Migrations/20230527215722_LicenseAndScrobble.Designer.cs similarity index 99% rename from API/Data/Migrations/20230527215722_LicenseAndScrobble.Designer.cs rename to Kavita.Database/Migrations/20230527215722_LicenseAndScrobble.Designer.cs index d1260ed6c..0efb91b0d 100644 --- a/API/Data/Migrations/20230527215722_LicenseAndScrobble.Designer.cs +++ b/Kavita.Database/Migrations/20230527215722_LicenseAndScrobble.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230527215722_LicenseAndScrobble")] diff --git a/API/Data/Migrations/20230527215722_LicenseAndScrobble.cs b/Kavita.Database/Migrations/20230527215722_LicenseAndScrobble.cs similarity index 99% rename from API/Data/Migrations/20230527215722_LicenseAndScrobble.cs rename to Kavita.Database/Migrations/20230527215722_LicenseAndScrobble.cs index e54f0ade9..fb57b6bee 100644 --- a/API/Data/Migrations/20230527215722_LicenseAndScrobble.cs +++ b/Kavita.Database/Migrations/20230527215722_LicenseAndScrobble.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class LicenseAndScrobble : Migration diff --git a/API/Data/Migrations/20230601172306_ScrobbleErrors.Designer.cs b/Kavita.Database/Migrations/20230601172306_ScrobbleErrors.Designer.cs similarity index 99% rename from API/Data/Migrations/20230601172306_ScrobbleErrors.Designer.cs rename to Kavita.Database/Migrations/20230601172306_ScrobbleErrors.Designer.cs index ddf9f8c6b..75881b18b 100644 --- a/API/Data/Migrations/20230601172306_ScrobbleErrors.Designer.cs +++ b/Kavita.Database/Migrations/20230601172306_ScrobbleErrors.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230601172306_ScrobbleErrors")] diff --git a/API/Data/Migrations/20230601172306_ScrobbleErrors.cs b/Kavita.Database/Migrations/20230601172306_ScrobbleErrors.cs similarity index 98% rename from API/Data/Migrations/20230601172306_ScrobbleErrors.cs rename to Kavita.Database/Migrations/20230601172306_ScrobbleErrors.cs index 22aeae714..94d628ed3 100644 --- a/API/Data/Migrations/20230601172306_ScrobbleErrors.cs +++ b/Kavita.Database/Migrations/20230601172306_ScrobbleErrors.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ScrobbleErrors : Migration diff --git a/API/Data/Migrations/20230612154313_ScrobbleEventProcessed.Designer.cs b/Kavita.Database/Migrations/20230612154313_ScrobbleEventProcessed.Designer.cs similarity index 99% rename from API/Data/Migrations/20230612154313_ScrobbleEventProcessed.Designer.cs rename to Kavita.Database/Migrations/20230612154313_ScrobbleEventProcessed.Designer.cs index ad8d11d07..9defe8546 100644 --- a/API/Data/Migrations/20230612154313_ScrobbleEventProcessed.Designer.cs +++ b/Kavita.Database/Migrations/20230612154313_ScrobbleEventProcessed.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230612154313_ScrobbleEventProcessed")] diff --git a/API/Data/Migrations/20230612154313_ScrobbleEventProcessed.cs b/Kavita.Database/Migrations/20230612154313_ScrobbleEventProcessed.cs similarity index 99% rename from API/Data/Migrations/20230612154313_ScrobbleEventProcessed.cs rename to Kavita.Database/Migrations/20230612154313_ScrobbleEventProcessed.cs index adfa1e1ce..71d499f00 100644 --- a/API/Data/Migrations/20230612154313_ScrobbleEventProcessed.cs +++ b/Kavita.Database/Migrations/20230612154313_ScrobbleEventProcessed.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ScrobbleEventProcessed : Migration diff --git a/API/Data/Migrations/20230615133219_ReviewTaglineAndOptInShares.Designer.cs b/Kavita.Database/Migrations/20230615133219_ReviewTaglineAndOptInShares.Designer.cs similarity index 99% rename from API/Data/Migrations/20230615133219_ReviewTaglineAndOptInShares.Designer.cs rename to Kavita.Database/Migrations/20230615133219_ReviewTaglineAndOptInShares.Designer.cs index 3da312853..eefd873f1 100644 --- a/API/Data/Migrations/20230615133219_ReviewTaglineAndOptInShares.Designer.cs +++ b/Kavita.Database/Migrations/20230615133219_ReviewTaglineAndOptInShares.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230615133219_ReviewTaglineAndOptInShares")] diff --git a/API/Data/Migrations/20230615133219_ReviewTaglineAndOptInShares.cs b/Kavita.Database/Migrations/20230615133219_ReviewTaglineAndOptInShares.cs similarity index 96% rename from API/Data/Migrations/20230615133219_ReviewTaglineAndOptInShares.cs rename to Kavita.Database/Migrations/20230615133219_ReviewTaglineAndOptInShares.cs index 7a44cce97..7b8e6d749 100644 --- a/API/Data/Migrations/20230615133219_ReviewTaglineAndOptInShares.cs +++ b/Kavita.Database/Migrations/20230615133219_ReviewTaglineAndOptInShares.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ReviewTaglineAndOptInShares : Migration diff --git a/API/Data/Migrations/20230618150728_ScrobbleHolds.Designer.cs b/Kavita.Database/Migrations/20230618150728_ScrobbleHolds.Designer.cs similarity index 99% rename from API/Data/Migrations/20230618150728_ScrobbleHolds.Designer.cs rename to Kavita.Database/Migrations/20230618150728_ScrobbleHolds.Designer.cs index 7773f41e0..5f6020e0c 100644 --- a/API/Data/Migrations/20230618150728_ScrobbleHolds.Designer.cs +++ b/Kavita.Database/Migrations/20230618150728_ScrobbleHolds.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230618150728_ScrobbleHolds")] diff --git a/API/Data/Migrations/20230618150728_ScrobbleHolds.cs b/Kavita.Database/Migrations/20230618150728_ScrobbleHolds.cs similarity index 98% rename from API/Data/Migrations/20230618150728_ScrobbleHolds.cs rename to Kavita.Database/Migrations/20230618150728_ScrobbleHolds.cs index 9023376d3..ea8cfa697 100644 --- a/API/Data/Migrations/20230618150728_ScrobbleHolds.cs +++ b/Kavita.Database/Migrations/20230618150728_ScrobbleHolds.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ScrobbleHolds : Migration diff --git a/API/Data/Migrations/20230621211421_RemoveUserLicense.Designer.cs b/Kavita.Database/Migrations/20230621211421_RemoveUserLicense.Designer.cs similarity index 99% rename from API/Data/Migrations/20230621211421_RemoveUserLicense.Designer.cs rename to Kavita.Database/Migrations/20230621211421_RemoveUserLicense.Designer.cs index 5ca2fb3b0..0af796fbc 100644 --- a/API/Data/Migrations/20230621211421_RemoveUserLicense.Designer.cs +++ b/Kavita.Database/Migrations/20230621211421_RemoveUserLicense.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230621211421_RemoveUserLicense")] diff --git a/API/Data/Migrations/20230621211421_RemoveUserLicense.cs b/Kavita.Database/Migrations/20230621211421_RemoveUserLicense.cs similarity index 96% rename from API/Data/Migrations/20230621211421_RemoveUserLicense.cs rename to Kavita.Database/Migrations/20230621211421_RemoveUserLicense.cs index 0c2d19a96..ad2ddb0e4 100644 --- a/API/Data/Migrations/20230621211421_RemoveUserLicense.cs +++ b/Kavita.Database/Migrations/20230621211421_RemoveUserLicense.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class RemoveUserLicense : Migration diff --git a/API/Data/Migrations/20230623192231_ScrobbleReview.Designer.cs b/Kavita.Database/Migrations/20230623192231_ScrobbleReview.Designer.cs similarity index 99% rename from API/Data/Migrations/20230623192231_ScrobbleReview.Designer.cs rename to Kavita.Database/Migrations/20230623192231_ScrobbleReview.Designer.cs index 2dc5d7c09..56cfd7049 100644 --- a/API/Data/Migrations/20230623192231_ScrobbleReview.Designer.cs +++ b/Kavita.Database/Migrations/20230623192231_ScrobbleReview.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230623192231_ScrobbleReview")] diff --git a/API/Data/Migrations/20230623192231_ScrobbleReview.cs b/Kavita.Database/Migrations/20230623192231_ScrobbleReview.cs similarity index 97% rename from API/Data/Migrations/20230623192231_ScrobbleReview.cs rename to Kavita.Database/Migrations/20230623192231_ScrobbleReview.cs index a35e658c2..8e1a98eda 100644 --- a/API/Data/Migrations/20230623192231_ScrobbleReview.cs +++ b/Kavita.Database/Migrations/20230623192231_ScrobbleReview.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ScrobbleReview : Migration diff --git a/API/Data/Migrations/20230715125951_OnDeckRemoval.Designer.cs b/Kavita.Database/Migrations/20230715125951_OnDeckRemoval.Designer.cs similarity index 99% rename from API/Data/Migrations/20230715125951_OnDeckRemoval.Designer.cs rename to Kavita.Database/Migrations/20230715125951_OnDeckRemoval.Designer.cs index 90035e9f0..a469e1ff2 100644 --- a/API/Data/Migrations/20230715125951_OnDeckRemoval.Designer.cs +++ b/Kavita.Database/Migrations/20230715125951_OnDeckRemoval.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230715125951_OnDeckRemoval")] diff --git a/API/Data/Migrations/20230715125951_OnDeckRemoval.cs b/Kavita.Database/Migrations/20230715125951_OnDeckRemoval.cs similarity index 98% rename from API/Data/Migrations/20230715125951_OnDeckRemoval.cs rename to Kavita.Database/Migrations/20230715125951_OnDeckRemoval.cs index 3cc27196f..e417080f4 100644 --- a/API/Data/Migrations/20230715125951_OnDeckRemoval.cs +++ b/Kavita.Database/Migrations/20230715125951_OnDeckRemoval.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class OnDeckRemoval : Migration diff --git a/API/Data/Migrations/20230719173458_PersonalToC.Designer.cs b/Kavita.Database/Migrations/20230719173458_PersonalToC.Designer.cs similarity index 99% rename from API/Data/Migrations/20230719173458_PersonalToC.Designer.cs rename to Kavita.Database/Migrations/20230719173458_PersonalToC.Designer.cs index 50e9ffd61..787a2351e 100644 --- a/API/Data/Migrations/20230719173458_PersonalToC.Designer.cs +++ b/Kavita.Database/Migrations/20230719173458_PersonalToC.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230719173458_PersonalToC")] diff --git a/API/Data/Migrations/20230719173458_PersonalToC.cs b/Kavita.Database/Migrations/20230719173458_PersonalToC.cs similarity index 98% rename from API/Data/Migrations/20230719173458_PersonalToC.cs rename to Kavita.Database/Migrations/20230719173458_PersonalToC.cs index c3eb9e025..ef80e73ca 100644 --- a/API/Data/Migrations/20230719173458_PersonalToC.cs +++ b/Kavita.Database/Migrations/20230719173458_PersonalToC.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class PersonalToC : Migration diff --git a/API/Data/Migrations/20230725133536_ChangeRatingScale.Designer.cs b/Kavita.Database/Migrations/20230725133536_ChangeRatingScale.Designer.cs similarity index 99% rename from API/Data/Migrations/20230725133536_ChangeRatingScale.Designer.cs rename to Kavita.Database/Migrations/20230725133536_ChangeRatingScale.Designer.cs index 8b5edb0ff..3dd1472ca 100644 --- a/API/Data/Migrations/20230725133536_ChangeRatingScale.Designer.cs +++ b/Kavita.Database/Migrations/20230725133536_ChangeRatingScale.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230725133536_ChangeRatingScale")] diff --git a/API/Data/Migrations/20230725133536_ChangeRatingScale.cs b/Kavita.Database/Migrations/20230725133536_ChangeRatingScale.cs similarity index 97% rename from API/Data/Migrations/20230725133536_ChangeRatingScale.cs rename to Kavita.Database/Migrations/20230725133536_ChangeRatingScale.cs index 4f97e008b..0e3bb00f5 100644 --- a/API/Data/Migrations/20230725133536_ChangeRatingScale.cs +++ b/Kavita.Database/Migrations/20230725133536_ChangeRatingScale.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ChangeRatingScale : Migration diff --git a/API/Data/Migrations/20230727175518_AddLocaleOnPrefs.Designer.cs b/Kavita.Database/Migrations/20230727175518_AddLocaleOnPrefs.Designer.cs similarity index 99% rename from API/Data/Migrations/20230727175518_AddLocaleOnPrefs.Designer.cs rename to Kavita.Database/Migrations/20230727175518_AddLocaleOnPrefs.Designer.cs index fb1afbeb9..a1520e27c 100644 --- a/API/Data/Migrations/20230727175518_AddLocaleOnPrefs.Designer.cs +++ b/Kavita.Database/Migrations/20230727175518_AddLocaleOnPrefs.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230727175518_AddLocaleOnPrefs")] diff --git a/API/Data/Migrations/20230727175518_AddLocaleOnPrefs.cs b/Kavita.Database/Migrations/20230727175518_AddLocaleOnPrefs.cs similarity index 95% rename from API/Data/Migrations/20230727175518_AddLocaleOnPrefs.cs rename to Kavita.Database/Migrations/20230727175518_AddLocaleOnPrefs.cs index 6b8d01bfe..4aa0ed181 100644 --- a/API/Data/Migrations/20230727175518_AddLocaleOnPrefs.cs +++ b/Kavita.Database/Migrations/20230727175518_AddLocaleOnPrefs.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class AddLocaleOnPrefs : Migration diff --git a/API/Data/Migrations/20230904184205_SmartFilters.Designer.cs b/Kavita.Database/Migrations/20230904184205_SmartFilters.Designer.cs similarity index 99% rename from API/Data/Migrations/20230904184205_SmartFilters.Designer.cs rename to Kavita.Database/Migrations/20230904184205_SmartFilters.Designer.cs index 2379ec2ad..dc3368593 100644 --- a/API/Data/Migrations/20230904184205_SmartFilters.Designer.cs +++ b/Kavita.Database/Migrations/20230904184205_SmartFilters.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230904184205_SmartFilters")] diff --git a/API/Data/Migrations/20230904184205_SmartFilters.cs b/Kavita.Database/Migrations/20230904184205_SmartFilters.cs similarity index 97% rename from API/Data/Migrations/20230904184205_SmartFilters.cs rename to Kavita.Database/Migrations/20230904184205_SmartFilters.cs index c902b907b..16c69bcc2 100644 --- a/API/Data/Migrations/20230904184205_SmartFilters.cs +++ b/Kavita.Database/Migrations/20230904184205_SmartFilters.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class SmartFilters : Migration diff --git a/API/Data/Migrations/20230908190713_DashboardStream.Designer.cs b/Kavita.Database/Migrations/20230908190713_DashboardStream.Designer.cs similarity index 99% rename from API/Data/Migrations/20230908190713_DashboardStream.Designer.cs rename to Kavita.Database/Migrations/20230908190713_DashboardStream.Designer.cs index 8e436f836..7c3bf5eb0 100644 --- a/API/Data/Migrations/20230908190713_DashboardStream.Designer.cs +++ b/Kavita.Database/Migrations/20230908190713_DashboardStream.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20230908190713_DashboardStream")] diff --git a/API/Data/Migrations/20230908190713_DashboardStream.cs b/Kavita.Database/Migrations/20230908190713_DashboardStream.cs similarity index 98% rename from API/Data/Migrations/20230908190713_DashboardStream.cs rename to Kavita.Database/Migrations/20230908190713_DashboardStream.cs index 10826c176..202e4ce5c 100644 --- a/API/Data/Migrations/20230908190713_DashboardStream.cs +++ b/Kavita.Database/Migrations/20230908190713_DashboardStream.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class DashboardStream : Migration diff --git a/API/Data/Migrations/20231013194957_SideNavStreamAndExternalSource.Designer.cs b/Kavita.Database/Migrations/20231013194957_SideNavStreamAndExternalSource.Designer.cs similarity index 99% rename from API/Data/Migrations/20231013194957_SideNavStreamAndExternalSource.Designer.cs rename to Kavita.Database/Migrations/20231013194957_SideNavStreamAndExternalSource.Designer.cs index 708bcb46e..5a2d349be 100644 --- a/API/Data/Migrations/20231013194957_SideNavStreamAndExternalSource.Designer.cs +++ b/Kavita.Database/Migrations/20231013194957_SideNavStreamAndExternalSource.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20231013194957_SideNavStreamAndExternalSource")] diff --git a/API/Data/Migrations/20231013194957_SideNavStreamAndExternalSource.cs b/Kavita.Database/Migrations/20231013194957_SideNavStreamAndExternalSource.cs similarity index 99% rename from API/Data/Migrations/20231013194957_SideNavStreamAndExternalSource.cs rename to Kavita.Database/Migrations/20231013194957_SideNavStreamAndExternalSource.cs index b8dd6111e..23bc08437 100644 --- a/API/Data/Migrations/20231013194957_SideNavStreamAndExternalSource.cs +++ b/Kavita.Database/Migrations/20231013194957_SideNavStreamAndExternalSource.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class SideNavStreamAndExternalSource : Migration diff --git a/API/Data/Migrations/20231113215006_LibraryFileTypes.Designer.cs b/Kavita.Database/Migrations/20231113215006_LibraryFileTypes.Designer.cs similarity index 99% rename from API/Data/Migrations/20231113215006_LibraryFileTypes.Designer.cs rename to Kavita.Database/Migrations/20231113215006_LibraryFileTypes.Designer.cs index ec955717c..4c70e9dc6 100644 --- a/API/Data/Migrations/20231113215006_LibraryFileTypes.Designer.cs +++ b/Kavita.Database/Migrations/20231113215006_LibraryFileTypes.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20231113215006_LibraryFileTypes")] diff --git a/API/Data/Migrations/20231113215006_LibraryFileTypes.cs b/Kavita.Database/Migrations/20231113215006_LibraryFileTypes.cs similarity index 97% rename from API/Data/Migrations/20231113215006_LibraryFileTypes.cs rename to Kavita.Database/Migrations/20231113215006_LibraryFileTypes.cs index 7fed106e7..35f721e67 100644 --- a/API/Data/Migrations/20231113215006_LibraryFileTypes.cs +++ b/Kavita.Database/Migrations/20231113215006_LibraryFileTypes.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class LibraryFileTypes : Migration diff --git a/API/Data/Migrations/20231117234829_LibraryExcludePatterns.Designer.cs b/Kavita.Database/Migrations/20231117234829_LibraryExcludePatterns.Designer.cs similarity index 99% rename from API/Data/Migrations/20231117234829_LibraryExcludePatterns.Designer.cs rename to Kavita.Database/Migrations/20231117234829_LibraryExcludePatterns.Designer.cs index b53aa8138..086860f75 100644 --- a/API/Data/Migrations/20231117234829_LibraryExcludePatterns.Designer.cs +++ b/Kavita.Database/Migrations/20231117234829_LibraryExcludePatterns.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20231117234829_LibraryExcludePatterns")] diff --git a/API/Data/Migrations/20231117234829_LibraryExcludePatterns.cs b/Kavita.Database/Migrations/20231117234829_LibraryExcludePatterns.cs similarity index 97% rename from API/Data/Migrations/20231117234829_LibraryExcludePatterns.cs rename to Kavita.Database/Migrations/20231117234829_LibraryExcludePatterns.cs index d1dd084f7..4c2bf0d0e 100644 --- a/API/Data/Migrations/20231117234829_LibraryExcludePatterns.cs +++ b/Kavita.Database/Migrations/20231117234829_LibraryExcludePatterns.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class LibraryExcludePatterns : Migration diff --git a/API/Data/Migrations/20240121223643_ExternalSeriesMetadata.Designer.cs b/Kavita.Database/Migrations/20240121223643_ExternalSeriesMetadata.Designer.cs similarity index 99% rename from API/Data/Migrations/20240121223643_ExternalSeriesMetadata.Designer.cs rename to Kavita.Database/Migrations/20240121223643_ExternalSeriesMetadata.Designer.cs index e7fdad65e..6c33c0af7 100644 --- a/API/Data/Migrations/20240121223643_ExternalSeriesMetadata.Designer.cs +++ b/Kavita.Database/Migrations/20240121223643_ExternalSeriesMetadata.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240121223643_ExternalSeriesMetadata")] diff --git a/API/Data/Migrations/20240121223643_ExternalSeriesMetadata.cs b/Kavita.Database/Migrations/20240121223643_ExternalSeriesMetadata.cs similarity index 99% rename from API/Data/Migrations/20240121223643_ExternalSeriesMetadata.cs rename to Kavita.Database/Migrations/20240121223643_ExternalSeriesMetadata.cs index 718332b9f..34af2125b 100644 --- a/API/Data/Migrations/20240121223643_ExternalSeriesMetadata.cs +++ b/Kavita.Database/Migrations/20240121223643_ExternalSeriesMetadata.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ExternalSeriesMetadata : Migration diff --git a/API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.Designer.cs b/Kavita.Database/Migrations/20240128153433_VolumeMinMaxNumbers.Designer.cs similarity index 99% rename from API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.Designer.cs rename to Kavita.Database/Migrations/20240128153433_VolumeMinMaxNumbers.Designer.cs index 730b40ec0..60a47bd35 100644 --- a/API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.Designer.cs +++ b/Kavita.Database/Migrations/20240128153433_VolumeMinMaxNumbers.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240128153433_VolumeMinMaxNumbers")] diff --git a/API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.cs b/Kavita.Database/Migrations/20240128153433_VolumeMinMaxNumbers.cs similarity index 96% rename from API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.cs rename to Kavita.Database/Migrations/20240128153433_VolumeMinMaxNumbers.cs index 491fd057f..ceea75856 100644 --- a/API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.cs +++ b/Kavita.Database/Migrations/20240128153433_VolumeMinMaxNumbers.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class VolumeMinMaxNumbers : Migration diff --git a/API/Data/Migrations/20240130190617_WantToReadFix.Designer.cs b/Kavita.Database/Migrations/20240130190617_WantToReadFix.Designer.cs similarity index 99% rename from API/Data/Migrations/20240130190617_WantToReadFix.Designer.cs rename to Kavita.Database/Migrations/20240130190617_WantToReadFix.Designer.cs index a4203171c..e0845bb4f 100644 --- a/API/Data/Migrations/20240130190617_WantToReadFix.Designer.cs +++ b/Kavita.Database/Migrations/20240130190617_WantToReadFix.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240130190617_WantToReadFix")] diff --git a/API/Data/Migrations/20240130190617_WantToReadFix.cs b/Kavita.Database/Migrations/20240130190617_WantToReadFix.cs similarity index 99% rename from API/Data/Migrations/20240130190617_WantToReadFix.cs rename to Kavita.Database/Migrations/20240130190617_WantToReadFix.cs index 386160db3..ba0c14c13 100644 --- a/API/Data/Migrations/20240130190617_WantToReadFix.cs +++ b/Kavita.Database/Migrations/20240130190617_WantToReadFix.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class WantToReadFix : Migration diff --git a/API/Data/Migrations/20240204141206_BlackListSeries.Designer.cs b/Kavita.Database/Migrations/20240204141206_BlackListSeries.Designer.cs similarity index 99% rename from API/Data/Migrations/20240204141206_BlackListSeries.Designer.cs rename to Kavita.Database/Migrations/20240204141206_BlackListSeries.Designer.cs index c399f13cc..757286f05 100644 --- a/API/Data/Migrations/20240204141206_BlackListSeries.Designer.cs +++ b/Kavita.Database/Migrations/20240204141206_BlackListSeries.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240204141206_BlackListSeries")] diff --git a/API/Data/Migrations/20240204141206_BlackListSeries.cs b/Kavita.Database/Migrations/20240204141206_BlackListSeries.cs similarity index 98% rename from API/Data/Migrations/20240204141206_BlackListSeries.cs rename to Kavita.Database/Migrations/20240204141206_BlackListSeries.cs index 9e051e5a7..3b3d959aa 100644 --- a/API/Data/Migrations/20240204141206_BlackListSeries.cs +++ b/Kavita.Database/Migrations/20240204141206_BlackListSeries.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class BlackListSeries : Migration diff --git a/API/Data/Migrations/20240205184724_ScrobbleEventError.Designer.cs b/Kavita.Database/Migrations/20240205184724_ScrobbleEventError.Designer.cs similarity index 99% rename from API/Data/Migrations/20240205184724_ScrobbleEventError.Designer.cs rename to Kavita.Database/Migrations/20240205184724_ScrobbleEventError.Designer.cs index df5692eb4..e22acda0c 100644 --- a/API/Data/Migrations/20240205184724_ScrobbleEventError.Designer.cs +++ b/Kavita.Database/Migrations/20240205184724_ScrobbleEventError.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240205184724_ScrobbleEventError")] diff --git a/API/Data/Migrations/20240205184724_ScrobbleEventError.cs b/Kavita.Database/Migrations/20240205184724_ScrobbleEventError.cs similarity index 97% rename from API/Data/Migrations/20240205184724_ScrobbleEventError.cs rename to Kavita.Database/Migrations/20240205184724_ScrobbleEventError.cs index 5c8071b18..c36bad7d5 100644 --- a/API/Data/Migrations/20240205184724_ScrobbleEventError.cs +++ b/Kavita.Database/Migrations/20240205184724_ScrobbleEventError.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ScrobbleEventError : Migration diff --git a/API/Data/Migrations/20240209224347_DBTweaks.Designer.cs b/Kavita.Database/Migrations/20240209224347_DBTweaks.Designer.cs similarity index 99% rename from API/Data/Migrations/20240209224347_DBTweaks.Designer.cs rename to Kavita.Database/Migrations/20240209224347_DBTweaks.Designer.cs index 0afb2e5cb..5a63295f4 100644 --- a/API/Data/Migrations/20240209224347_DBTweaks.Designer.cs +++ b/Kavita.Database/Migrations/20240209224347_DBTweaks.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240209224347_DBTweaks")] diff --git a/API/Data/Migrations/20240209224347_DBTweaks.cs b/Kavita.Database/Migrations/20240209224347_DBTweaks.cs similarity index 95% rename from API/Data/Migrations/20240209224347_DBTweaks.cs rename to Kavita.Database/Migrations/20240209224347_DBTweaks.cs index 797905930..7adaac1d6 100644 --- a/API/Data/Migrations/20240209224347_DBTweaks.cs +++ b/Kavita.Database/Migrations/20240209224347_DBTweaks.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class DBTweaks : Migration diff --git a/API/Data/Migrations/20240214232436_ChapterNumber.Designer.cs b/Kavita.Database/Migrations/20240214232436_ChapterNumber.Designer.cs similarity index 99% rename from API/Data/Migrations/20240214232436_ChapterNumber.Designer.cs rename to Kavita.Database/Migrations/20240214232436_ChapterNumber.Designer.cs index d770ccbbd..3956e0be0 100644 --- a/API/Data/Migrations/20240214232436_ChapterNumber.Designer.cs +++ b/Kavita.Database/Migrations/20240214232436_ChapterNumber.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240214232436_ChapterNumber")] diff --git a/API/Data/Migrations/20240214232436_ChapterNumber.cs b/Kavita.Database/Migrations/20240214232436_ChapterNumber.cs similarity index 96% rename from API/Data/Migrations/20240214232436_ChapterNumber.cs rename to Kavita.Database/Migrations/20240214232436_ChapterNumber.cs index c1e277d58..d6062fedc 100644 --- a/API/Data/Migrations/20240214232436_ChapterNumber.cs +++ b/Kavita.Database/Migrations/20240214232436_ChapterNumber.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ChapterNumber : Migration diff --git a/API/Data/Migrations/20240216000223_MangaFileNameTemp.Designer.cs b/Kavita.Database/Migrations/20240216000223_MangaFileNameTemp.Designer.cs similarity index 99% rename from API/Data/Migrations/20240216000223_MangaFileNameTemp.Designer.cs rename to Kavita.Database/Migrations/20240216000223_MangaFileNameTemp.Designer.cs index 7709d9afa..c89e0225c 100644 --- a/API/Data/Migrations/20240216000223_MangaFileNameTemp.Designer.cs +++ b/Kavita.Database/Migrations/20240216000223_MangaFileNameTemp.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240216000223_MangaFileNameTemp")] diff --git a/API/Data/Migrations/20240216000223_MangaFileNameTemp.cs b/Kavita.Database/Migrations/20240216000223_MangaFileNameTemp.cs similarity index 94% rename from API/Data/Migrations/20240216000223_MangaFileNameTemp.cs rename to Kavita.Database/Migrations/20240216000223_MangaFileNameTemp.cs index 8a14c912c..925550971 100644 --- a/API/Data/Migrations/20240216000223_MangaFileNameTemp.cs +++ b/Kavita.Database/Migrations/20240216000223_MangaFileNameTemp.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class MangaFileNameTemp : Migration diff --git a/API/Data/Migrations/20240222125420_ChapterIssueSort.Designer.cs b/Kavita.Database/Migrations/20240222125420_ChapterIssueSort.Designer.cs similarity index 99% rename from API/Data/Migrations/20240222125420_ChapterIssueSort.Designer.cs rename to Kavita.Database/Migrations/20240222125420_ChapterIssueSort.Designer.cs index 68c1a12e5..3f600cc76 100644 --- a/API/Data/Migrations/20240222125420_ChapterIssueSort.Designer.cs +++ b/Kavita.Database/Migrations/20240222125420_ChapterIssueSort.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240222125420_ChapterIssueSort")] diff --git a/API/Data/Migrations/20240222125420_ChapterIssueSort.cs b/Kavita.Database/Migrations/20240222125420_ChapterIssueSort.cs similarity index 95% rename from API/Data/Migrations/20240222125420_ChapterIssueSort.cs rename to Kavita.Database/Migrations/20240222125420_ChapterIssueSort.cs index 0689a8e88..23e1aca2d 100644 --- a/API/Data/Migrations/20240222125420_ChapterIssueSort.cs +++ b/Kavita.Database/Migrations/20240222125420_ChapterIssueSort.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ChapterIssueSort : Migration diff --git a/API/Data/Migrations/20240225235816_VolumeLookupName.Designer.cs b/Kavita.Database/Migrations/20240225235816_VolumeLookupName.Designer.cs similarity index 99% rename from API/Data/Migrations/20240225235816_VolumeLookupName.Designer.cs rename to Kavita.Database/Migrations/20240225235816_VolumeLookupName.Designer.cs index c7f646f73..cbb63d0e7 100644 --- a/API/Data/Migrations/20240225235816_VolumeLookupName.Designer.cs +++ b/Kavita.Database/Migrations/20240225235816_VolumeLookupName.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240225235816_VolumeLookupName")] diff --git a/API/Data/Migrations/20240225235816_VolumeLookupName.cs b/Kavita.Database/Migrations/20240225235816_VolumeLookupName.cs similarity index 94% rename from API/Data/Migrations/20240225235816_VolumeLookupName.cs rename to Kavita.Database/Migrations/20240225235816_VolumeLookupName.cs index 3d42e9645..85bae38be 100644 --- a/API/Data/Migrations/20240225235816_VolumeLookupName.cs +++ b/Kavita.Database/Migrations/20240225235816_VolumeLookupName.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class VolumeLookupName : Migration diff --git a/API/Data/Migrations/20240309140117_SeriesImprints.Designer.cs b/Kavita.Database/Migrations/20240309140117_SeriesImprints.Designer.cs similarity index 99% rename from API/Data/Migrations/20240309140117_SeriesImprints.Designer.cs rename to Kavita.Database/Migrations/20240309140117_SeriesImprints.Designer.cs index d99650e86..e4f08d6aa 100644 --- a/API/Data/Migrations/20240309140117_SeriesImprints.Designer.cs +++ b/Kavita.Database/Migrations/20240309140117_SeriesImprints.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240309140117_SeriesImprints")] diff --git a/API/Data/Migrations/20240309140117_SeriesImprints.cs b/Kavita.Database/Migrations/20240309140117_SeriesImprints.cs similarity index 95% rename from API/Data/Migrations/20240309140117_SeriesImprints.cs rename to Kavita.Database/Migrations/20240309140117_SeriesImprints.cs index a48ac7c48..c81f7b331 100644 --- a/API/Data/Migrations/20240309140117_SeriesImprints.cs +++ b/Kavita.Database/Migrations/20240309140117_SeriesImprints.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class SeriesImprints : Migration diff --git a/API/Data/Migrations/20240313112552_SeriesLowestFolderPath.Designer.cs b/Kavita.Database/Migrations/20240313112552_SeriesLowestFolderPath.Designer.cs similarity index 99% rename from API/Data/Migrations/20240313112552_SeriesLowestFolderPath.Designer.cs rename to Kavita.Database/Migrations/20240313112552_SeriesLowestFolderPath.Designer.cs index 707d6ea0a..f852fdedb 100644 --- a/API/Data/Migrations/20240313112552_SeriesLowestFolderPath.Designer.cs +++ b/Kavita.Database/Migrations/20240313112552_SeriesLowestFolderPath.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240313112552_SeriesLowestFolderPath")] diff --git a/API/Data/Migrations/20240313112552_SeriesLowestFolderPath.cs b/Kavita.Database/Migrations/20240313112552_SeriesLowestFolderPath.cs similarity index 95% rename from API/Data/Migrations/20240313112552_SeriesLowestFolderPath.cs rename to Kavita.Database/Migrations/20240313112552_SeriesLowestFolderPath.cs index e138bd8f1..ead8ac337 100644 --- a/API/Data/Migrations/20240313112552_SeriesLowestFolderPath.cs +++ b/Kavita.Database/Migrations/20240313112552_SeriesLowestFolderPath.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class SeriesLowestFolderPath : Migration diff --git a/API/Data/Migrations/20240314194402_TeamsAndLocations.Designer.cs b/Kavita.Database/Migrations/20240314194402_TeamsAndLocations.Designer.cs similarity index 99% rename from API/Data/Migrations/20240314194402_TeamsAndLocations.Designer.cs rename to Kavita.Database/Migrations/20240314194402_TeamsAndLocations.Designer.cs index 21616f684..da8dc68f5 100644 --- a/API/Data/Migrations/20240314194402_TeamsAndLocations.Designer.cs +++ b/Kavita.Database/Migrations/20240314194402_TeamsAndLocations.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240314194402_TeamsAndLocations")] diff --git a/API/Data/Migrations/20240314194402_TeamsAndLocations.cs b/Kavita.Database/Migrations/20240314194402_TeamsAndLocations.cs similarity index 96% rename from API/Data/Migrations/20240314194402_TeamsAndLocations.cs rename to Kavita.Database/Migrations/20240314194402_TeamsAndLocations.cs index dca377c99..7f96dd655 100644 --- a/API/Data/Migrations/20240314194402_TeamsAndLocations.cs +++ b/Kavita.Database/Migrations/20240314194402_TeamsAndLocations.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class TeamsAndLocations : Migration diff --git a/API/Data/Migrations/20240321173812_UserMalToken.Designer.cs b/Kavita.Database/Migrations/20240321173812_UserMalToken.Designer.cs similarity index 99% rename from API/Data/Migrations/20240321173812_UserMalToken.Designer.cs rename to Kavita.Database/Migrations/20240321173812_UserMalToken.Designer.cs index ee182676d..aac2b7c5d 100644 --- a/API/Data/Migrations/20240321173812_UserMalToken.Designer.cs +++ b/Kavita.Database/Migrations/20240321173812_UserMalToken.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240321173812_UserMalToken")] diff --git a/API/Data/Migrations/20240321173812_UserMalToken.cs b/Kavita.Database/Migrations/20240321173812_UserMalToken.cs similarity index 96% rename from API/Data/Migrations/20240321173812_UserMalToken.cs rename to Kavita.Database/Migrations/20240321173812_UserMalToken.cs index f1b1d3caa..37810eae8 100644 --- a/API/Data/Migrations/20240321173812_UserMalToken.cs +++ b/Kavita.Database/Migrations/20240321173812_UserMalToken.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class UserMalToken : Migration diff --git a/API/Data/Migrations/20240328130057_PdfSettings.Designer.cs b/Kavita.Database/Migrations/20240328130057_PdfSettings.Designer.cs similarity index 99% rename from API/Data/Migrations/20240328130057_PdfSettings.Designer.cs rename to Kavita.Database/Migrations/20240328130057_PdfSettings.Designer.cs index cba2d534f..a11582887 100644 --- a/API/Data/Migrations/20240328130057_PdfSettings.Designer.cs +++ b/Kavita.Database/Migrations/20240328130057_PdfSettings.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240328130057_PdfSettings")] diff --git a/API/Data/Migrations/20240328130057_PdfSettings.cs b/Kavita.Database/Migrations/20240328130057_PdfSettings.cs similarity index 97% rename from API/Data/Migrations/20240328130057_PdfSettings.cs rename to Kavita.Database/Migrations/20240328130057_PdfSettings.cs index 699875968..a0a33bbfb 100644 --- a/API/Data/Migrations/20240328130057_PdfSettings.cs +++ b/Kavita.Database/Migrations/20240328130057_PdfSettings.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class PdfSettings : Migration diff --git a/API/Data/Migrations/20240331172900_UserBasedCollections.Designer.cs b/Kavita.Database/Migrations/20240331172900_UserBasedCollections.Designer.cs similarity index 99% rename from API/Data/Migrations/20240331172900_UserBasedCollections.Designer.cs rename to Kavita.Database/Migrations/20240331172900_UserBasedCollections.Designer.cs index 5527a0fbb..4a17378af 100644 --- a/API/Data/Migrations/20240331172900_UserBasedCollections.Designer.cs +++ b/Kavita.Database/Migrations/20240331172900_UserBasedCollections.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240331172900_UserBasedCollections")] diff --git a/API/Data/Migrations/20240331172900_UserBasedCollections.cs b/Kavita.Database/Migrations/20240331172900_UserBasedCollections.cs similarity index 99% rename from API/Data/Migrations/20240331172900_UserBasedCollections.cs rename to Kavita.Database/Migrations/20240331172900_UserBasedCollections.cs index c5a376bd8..163c351bd 100644 --- a/API/Data/Migrations/20240331172900_UserBasedCollections.cs +++ b/Kavita.Database/Migrations/20240331172900_UserBasedCollections.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class UserBasedCollections : Migration diff --git a/API/Data/Migrations/20240418163829_ChapterSortOrderLock.Designer.cs b/Kavita.Database/Migrations/20240418163829_ChapterSortOrderLock.Designer.cs similarity index 99% rename from API/Data/Migrations/20240418163829_ChapterSortOrderLock.Designer.cs rename to Kavita.Database/Migrations/20240418163829_ChapterSortOrderLock.Designer.cs index 3cd3291b2..a65466722 100644 --- a/API/Data/Migrations/20240418163829_ChapterSortOrderLock.Designer.cs +++ b/Kavita.Database/Migrations/20240418163829_ChapterSortOrderLock.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240418163829_ChapterSortOrderLock")] diff --git a/API/Data/Migrations/20240418163829_ChapterSortOrderLock.cs b/Kavita.Database/Migrations/20240418163829_ChapterSortOrderLock.cs similarity index 96% rename from API/Data/Migrations/20240418163829_ChapterSortOrderLock.cs rename to Kavita.Database/Migrations/20240418163829_ChapterSortOrderLock.cs index 197085b0c..69579c2f0 100644 --- a/API/Data/Migrations/20240418163829_ChapterSortOrderLock.cs +++ b/Kavita.Database/Migrations/20240418163829_ChapterSortOrderLock.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ChapterSortOrderLock : Migration diff --git a/API/Data/Migrations/20240503120147_SmartCollectionFields.Designer.cs b/Kavita.Database/Migrations/20240503120147_SmartCollectionFields.Designer.cs similarity index 99% rename from API/Data/Migrations/20240503120147_SmartCollectionFields.Designer.cs rename to Kavita.Database/Migrations/20240503120147_SmartCollectionFields.Designer.cs index 1dff0c0e5..3bd473401 100644 --- a/API/Data/Migrations/20240503120147_SmartCollectionFields.Designer.cs +++ b/Kavita.Database/Migrations/20240503120147_SmartCollectionFields.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240503120147_SmartCollectionFields")] diff --git a/API/Data/Migrations/20240503120147_SmartCollectionFields.cs b/Kavita.Database/Migrations/20240503120147_SmartCollectionFields.cs similarity index 96% rename from API/Data/Migrations/20240503120147_SmartCollectionFields.cs rename to Kavita.Database/Migrations/20240503120147_SmartCollectionFields.cs index f0b6ed693..c4d2f7228 100644 --- a/API/Data/Migrations/20240503120147_SmartCollectionFields.cs +++ b/Kavita.Database/Migrations/20240503120147_SmartCollectionFields.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class SmartCollectionFields : Migration diff --git a/API/Data/Migrations/20240510134030_SiteThemeFields.Designer.cs b/Kavita.Database/Migrations/20240510134030_SiteThemeFields.Designer.cs similarity index 99% rename from API/Data/Migrations/20240510134030_SiteThemeFields.Designer.cs rename to Kavita.Database/Migrations/20240510134030_SiteThemeFields.Designer.cs index c88a1628f..1f6e57714 100644 --- a/API/Data/Migrations/20240510134030_SiteThemeFields.Designer.cs +++ b/Kavita.Database/Migrations/20240510134030_SiteThemeFields.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240510134030_SiteThemeFields")] diff --git a/API/Data/Migrations/20240510134030_SiteThemeFields.cs b/Kavita.Database/Migrations/20240510134030_SiteThemeFields.cs similarity index 98% rename from API/Data/Migrations/20240510134030_SiteThemeFields.cs rename to Kavita.Database/Migrations/20240510134030_SiteThemeFields.cs index 36171fa0a..8f91d66ea 100644 --- a/API/Data/Migrations/20240510134030_SiteThemeFields.cs +++ b/Kavita.Database/Migrations/20240510134030_SiteThemeFields.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class SiteThemeFields : Migration diff --git a/API/Data/Migrations/20240704144224_PersonFields.Designer.cs b/Kavita.Database/Migrations/20240704144224_PersonFields.Designer.cs similarity index 99% rename from API/Data/Migrations/20240704144224_PersonFields.Designer.cs rename to Kavita.Database/Migrations/20240704144224_PersonFields.Designer.cs index ddc41d811..ce6ddc6da 100644 --- a/API/Data/Migrations/20240704144224_PersonFields.Designer.cs +++ b/Kavita.Database/Migrations/20240704144224_PersonFields.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240704144224_PersonFields")] diff --git a/API/Data/Migrations/20240704144224_PersonFields.cs b/Kavita.Database/Migrations/20240704144224_PersonFields.cs similarity index 98% rename from API/Data/Migrations/20240704144224_PersonFields.cs rename to Kavita.Database/Migrations/20240704144224_PersonFields.cs index 2d30696ce..5c93e753b 100644 --- a/API/Data/Migrations/20240704144224_PersonFields.cs +++ b/Kavita.Database/Migrations/20240704144224_PersonFields.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class PersonFields : Migration diff --git a/API/Data/Migrations/20240808100353_CoverPrimaryColors.Designer.cs b/Kavita.Database/Migrations/20240808100353_CoverPrimaryColors.Designer.cs similarity index 99% rename from API/Data/Migrations/20240808100353_CoverPrimaryColors.Designer.cs rename to Kavita.Database/Migrations/20240808100353_CoverPrimaryColors.Designer.cs index d105ece92..27d8a89ea 100644 --- a/API/Data/Migrations/20240808100353_CoverPrimaryColors.Designer.cs +++ b/Kavita.Database/Migrations/20240808100353_CoverPrimaryColors.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240808100353_CoverPrimaryColors")] diff --git a/API/Data/Migrations/20240808100353_CoverPrimaryColors.cs b/Kavita.Database/Migrations/20240808100353_CoverPrimaryColors.cs similarity index 99% rename from API/Data/Migrations/20240808100353_CoverPrimaryColors.cs rename to Kavita.Database/Migrations/20240808100353_CoverPrimaryColors.cs index c69c906b0..d4f29ff81 100644 --- a/API/Data/Migrations/20240808100353_CoverPrimaryColors.cs +++ b/Kavita.Database/Migrations/20240808100353_CoverPrimaryColors.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class CoverPrimaryColors : Migration diff --git a/API/Data/Migrations/20240811154857_ChapterMetadataLocks.Designer.cs b/Kavita.Database/Migrations/20240811154857_ChapterMetadataLocks.Designer.cs similarity index 99% rename from API/Data/Migrations/20240811154857_ChapterMetadataLocks.Designer.cs rename to Kavita.Database/Migrations/20240811154857_ChapterMetadataLocks.Designer.cs index 07723e833..6cb6a9202 100644 --- a/API/Data/Migrations/20240811154857_ChapterMetadataLocks.Designer.cs +++ b/Kavita.Database/Migrations/20240811154857_ChapterMetadataLocks.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240811154857_ChapterMetadataLocks")] diff --git a/API/Data/Migrations/20240811154857_ChapterMetadataLocks.cs b/Kavita.Database/Migrations/20240811154857_ChapterMetadataLocks.cs similarity index 99% rename from API/Data/Migrations/20240811154857_ChapterMetadataLocks.cs rename to Kavita.Database/Migrations/20240811154857_ChapterMetadataLocks.cs index b0b58b3b3..219cce059 100644 --- a/API/Data/Migrations/20240811154857_ChapterMetadataLocks.cs +++ b/Kavita.Database/Migrations/20240811154857_ChapterMetadataLocks.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ChapterMetadataLocks : Migration diff --git a/API/Data/Migrations/20240813194728_VolumeCoverLocked.Designer.cs b/Kavita.Database/Migrations/20240813194728_VolumeCoverLocked.Designer.cs similarity index 99% rename from API/Data/Migrations/20240813194728_VolumeCoverLocked.Designer.cs rename to Kavita.Database/Migrations/20240813194728_VolumeCoverLocked.Designer.cs index 1471c1de7..9567f8ad6 100644 --- a/API/Data/Migrations/20240813194728_VolumeCoverLocked.Designer.cs +++ b/Kavita.Database/Migrations/20240813194728_VolumeCoverLocked.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240813194728_VolumeCoverLocked")] diff --git a/API/Data/Migrations/20240813194728_VolumeCoverLocked.cs b/Kavita.Database/Migrations/20240813194728_VolumeCoverLocked.cs similarity index 95% rename from API/Data/Migrations/20240813194728_VolumeCoverLocked.cs rename to Kavita.Database/Migrations/20240813194728_VolumeCoverLocked.cs index c9127ae6a..2bcef093b 100644 --- a/API/Data/Migrations/20240813194728_VolumeCoverLocked.cs +++ b/Kavita.Database/Migrations/20240813194728_VolumeCoverLocked.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class VolumeCoverLocked : Migration diff --git a/API/Data/Migrations/20240917180034_AvgReadingTimeFloat.Designer.cs b/Kavita.Database/Migrations/20240917180034_AvgReadingTimeFloat.Designer.cs similarity index 99% rename from API/Data/Migrations/20240917180034_AvgReadingTimeFloat.Designer.cs rename to Kavita.Database/Migrations/20240917180034_AvgReadingTimeFloat.Designer.cs index f9b858de5..1d34279bb 100644 --- a/API/Data/Migrations/20240917180034_AvgReadingTimeFloat.Designer.cs +++ b/Kavita.Database/Migrations/20240917180034_AvgReadingTimeFloat.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20240917180034_AvgReadingTimeFloat")] diff --git a/API/Data/Migrations/20240917180034_AvgReadingTimeFloat.cs b/Kavita.Database/Migrations/20240917180034_AvgReadingTimeFloat.cs similarity index 98% rename from API/Data/Migrations/20240917180034_AvgReadingTimeFloat.cs rename to Kavita.Database/Migrations/20240917180034_AvgReadingTimeFloat.cs index 70e9238ec..ae8ce5b48 100644 --- a/API/Data/Migrations/20240917180034_AvgReadingTimeFloat.cs +++ b/Kavita.Database/Migrations/20240917180034_AvgReadingTimeFloat.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class AvgReadingTimeFloat : Migration diff --git a/API/Data/Migrations/20241011143144_PeopleOverhaulPart1.Designer.cs b/Kavita.Database/Migrations/20241011143144_PeopleOverhaulPart1.Designer.cs similarity index 99% rename from API/Data/Migrations/20241011143144_PeopleOverhaulPart1.Designer.cs rename to Kavita.Database/Migrations/20241011143144_PeopleOverhaulPart1.Designer.cs index 3865e6007..cd4aeaeac 100644 --- a/API/Data/Migrations/20241011143144_PeopleOverhaulPart1.Designer.cs +++ b/Kavita.Database/Migrations/20241011143144_PeopleOverhaulPart1.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20241011143144_PeopleOverhaulPart1")] diff --git a/API/Data/Migrations/20241011143144_PeopleOverhaulPart1.cs b/Kavita.Database/Migrations/20241011143144_PeopleOverhaulPart1.cs similarity index 99% rename from API/Data/Migrations/20241011143144_PeopleOverhaulPart1.cs rename to Kavita.Database/Migrations/20241011143144_PeopleOverhaulPart1.cs index 1bf0cf6c4..d83f25f06 100644 --- a/API/Data/Migrations/20241011143144_PeopleOverhaulPart1.cs +++ b/Kavita.Database/Migrations/20241011143144_PeopleOverhaulPart1.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class PeopleOverhaulPart1 : Migration diff --git a/API/Data/Migrations/20241011152321_PeopleOverhaulPart2.Designer.cs b/Kavita.Database/Migrations/20241011152321_PeopleOverhaulPart2.Designer.cs similarity index 99% rename from API/Data/Migrations/20241011152321_PeopleOverhaulPart2.Designer.cs rename to Kavita.Database/Migrations/20241011152321_PeopleOverhaulPart2.Designer.cs index bbbf0f989..6205bf3f2 100644 --- a/API/Data/Migrations/20241011152321_PeopleOverhaulPart2.Designer.cs +++ b/Kavita.Database/Migrations/20241011152321_PeopleOverhaulPart2.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20241011152321_PeopleOverhaulPart2")] diff --git a/API/Data/Migrations/20241011152321_PeopleOverhaulPart2.cs b/Kavita.Database/Migrations/20241011152321_PeopleOverhaulPart2.cs similarity index 97% rename from API/Data/Migrations/20241011152321_PeopleOverhaulPart2.cs rename to Kavita.Database/Migrations/20241011152321_PeopleOverhaulPart2.cs index 4fd8e4b8d..caf1fc873 100644 --- a/API/Data/Migrations/20241011152321_PeopleOverhaulPart2.cs +++ b/Kavita.Database/Migrations/20241011152321_PeopleOverhaulPart2.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class PeopleOverhaulPart2 : Migration diff --git a/API/Data/Migrations/20241011172428_PeopleOverhaulPart3.Designer.cs b/Kavita.Database/Migrations/20241011172428_PeopleOverhaulPart3.Designer.cs similarity index 99% rename from API/Data/Migrations/20241011172428_PeopleOverhaulPart3.Designer.cs rename to Kavita.Database/Migrations/20241011172428_PeopleOverhaulPart3.Designer.cs index 6f76df92c..a8062c1d1 100644 --- a/API/Data/Migrations/20241011172428_PeopleOverhaulPart3.Designer.cs +++ b/Kavita.Database/Migrations/20241011172428_PeopleOverhaulPart3.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20241011172428_PeopleOverhaulPart3")] diff --git a/API/Data/Migrations/20241011172428_PeopleOverhaulPart3.cs b/Kavita.Database/Migrations/20241011172428_PeopleOverhaulPart3.cs similarity index 98% rename from API/Data/Migrations/20241011172428_PeopleOverhaulPart3.cs rename to Kavita.Database/Migrations/20241011172428_PeopleOverhaulPart3.cs index 13aa9e050..8f1b55171 100644 --- a/API/Data/Migrations/20241011172428_PeopleOverhaulPart3.cs +++ b/Kavita.Database/Migrations/20241011172428_PeopleOverhaulPart3.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class PeopleOverhaulPart3 : Migration diff --git a/API/Data/Migrations/20250105180131_SeriesDontMatchAndBlacklist.Designer.cs b/Kavita.Database/Migrations/20250105180131_SeriesDontMatchAndBlacklist.Designer.cs similarity index 99% rename from API/Data/Migrations/20250105180131_SeriesDontMatchAndBlacklist.Designer.cs rename to Kavita.Database/Migrations/20250105180131_SeriesDontMatchAndBlacklist.Designer.cs index a5158ebc1..7bda1a134 100644 --- a/API/Data/Migrations/20250105180131_SeriesDontMatchAndBlacklist.Designer.cs +++ b/Kavita.Database/Migrations/20250105180131_SeriesDontMatchAndBlacklist.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250105180131_SeriesDontMatchAndBlacklist")] diff --git a/API/Data/Migrations/20250105180131_SeriesDontMatchAndBlacklist.cs b/Kavita.Database/Migrations/20250105180131_SeriesDontMatchAndBlacklist.cs similarity index 96% rename from API/Data/Migrations/20250105180131_SeriesDontMatchAndBlacklist.cs rename to Kavita.Database/Migrations/20250105180131_SeriesDontMatchAndBlacklist.cs index ab80f0621..9e1a7c6ec 100644 --- a/API/Data/Migrations/20250105180131_SeriesDontMatchAndBlacklist.cs +++ b/Kavita.Database/Migrations/20250105180131_SeriesDontMatchAndBlacklist.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class SeriesDontMatchAndBlacklist : Migration diff --git a/API/Data/Migrations/20250109173537_EmailHistory.Designer.cs b/Kavita.Database/Migrations/20250109173537_EmailHistory.Designer.cs similarity index 99% rename from API/Data/Migrations/20250109173537_EmailHistory.Designer.cs rename to Kavita.Database/Migrations/20250109173537_EmailHistory.Designer.cs index ff3212562..69cfdba3a 100644 --- a/API/Data/Migrations/20250109173537_EmailHistory.Designer.cs +++ b/Kavita.Database/Migrations/20250109173537_EmailHistory.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250109173537_EmailHistory")] diff --git a/API/Data/Migrations/20250109173537_EmailHistory.cs b/Kavita.Database/Migrations/20250109173537_EmailHistory.cs similarity index 98% rename from API/Data/Migrations/20250109173537_EmailHistory.cs rename to Kavita.Database/Migrations/20250109173537_EmailHistory.cs index b31bf20c3..57b4a7125 100644 --- a/API/Data/Migrations/20250109173537_EmailHistory.cs +++ b/Kavita.Database/Migrations/20250109173537_EmailHistory.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class EmailHistory : Migration diff --git a/API/Data/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.Designer.cs b/Kavita.Database/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.Designer.cs similarity index 99% rename from API/Data/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.Designer.cs rename to Kavita.Database/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.Designer.cs index 835510a1e..b2ec168a6 100644 --- a/API/Data/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.Designer.cs +++ b/Kavita.Database/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250202163454_KavitaPlusUserAndMetadataSettings")] diff --git a/API/Data/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.cs b/Kavita.Database/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.cs similarity index 99% rename from API/Data/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.cs rename to Kavita.Database/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.cs index b23d7896b..657da1dbf 100644 --- a/API/Data/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.cs +++ b/Kavita.Database/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class KavitaPlusUserAndMetadataSettings : Migration diff --git a/API/Data/Migrations/20250208200843_MoreMetadtaSettings.Designer.cs b/Kavita.Database/Migrations/20250208200843_MoreMetadtaSettings.Designer.cs similarity index 99% rename from API/Data/Migrations/20250208200843_MoreMetadtaSettings.Designer.cs rename to Kavita.Database/Migrations/20250208200843_MoreMetadtaSettings.Designer.cs index 9aaa63101..6a7b99290 100644 --- a/API/Data/Migrations/20250208200843_MoreMetadtaSettings.Designer.cs +++ b/Kavita.Database/Migrations/20250208200843_MoreMetadtaSettings.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250208200843_MoreMetadtaSettings")] diff --git a/API/Data/Migrations/20250208200843_MoreMetadtaSettings.cs b/Kavita.Database/Migrations/20250208200843_MoreMetadtaSettings.cs similarity index 98% rename from API/Data/Migrations/20250208200843_MoreMetadtaSettings.cs rename to Kavita.Database/Migrations/20250208200843_MoreMetadtaSettings.cs index 70e42cd11..6aecb7d71 100644 --- a/API/Data/Migrations/20250208200843_MoreMetadtaSettings.cs +++ b/Kavita.Database/Migrations/20250208200843_MoreMetadtaSettings.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class MoreMetadtaSettings : Migration diff --git a/API/Data/Migrations/20250328125012_AutomaticWebtoonReaderMode.Designer.cs b/Kavita.Database/Migrations/20250328125012_AutomaticWebtoonReaderMode.Designer.cs similarity index 99% rename from API/Data/Migrations/20250328125012_AutomaticWebtoonReaderMode.Designer.cs rename to Kavita.Database/Migrations/20250328125012_AutomaticWebtoonReaderMode.Designer.cs index be3d5e3f9..fcf3d7e0c 100644 --- a/API/Data/Migrations/20250328125012_AutomaticWebtoonReaderMode.Designer.cs +++ b/Kavita.Database/Migrations/20250328125012_AutomaticWebtoonReaderMode.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250328125012_AutomaticWebtoonReaderMode")] diff --git a/API/Data/Migrations/20250328125012_AutomaticWebtoonReaderMode.cs b/Kavita.Database/Migrations/20250328125012_AutomaticWebtoonReaderMode.cs similarity index 95% rename from API/Data/Migrations/20250328125012_AutomaticWebtoonReaderMode.cs rename to Kavita.Database/Migrations/20250328125012_AutomaticWebtoonReaderMode.cs index 38b772811..508c6f0f8 100644 --- a/API/Data/Migrations/20250328125012_AutomaticWebtoonReaderMode.cs +++ b/Kavita.Database/Migrations/20250328125012_AutomaticWebtoonReaderMode.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class AutomaticWebtoonReaderMode : Migration diff --git a/API/Data/Migrations/20250408222330_ScrobbleGenerationDbCapture.Designer.cs b/Kavita.Database/Migrations/20250408222330_ScrobbleGenerationDbCapture.Designer.cs similarity index 99% rename from API/Data/Migrations/20250408222330_ScrobbleGenerationDbCapture.Designer.cs rename to Kavita.Database/Migrations/20250408222330_ScrobbleGenerationDbCapture.Designer.cs index 53e450b3b..2163133b5 100644 --- a/API/Data/Migrations/20250408222330_ScrobbleGenerationDbCapture.Designer.cs +++ b/Kavita.Database/Migrations/20250408222330_ScrobbleGenerationDbCapture.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250408222330_ScrobbleGenerationDbCapture")] diff --git a/API/Data/Migrations/20250408222330_ScrobbleGenerationDbCapture.cs b/Kavita.Database/Migrations/20250408222330_ScrobbleGenerationDbCapture.cs similarity index 97% rename from API/Data/Migrations/20250408222330_ScrobbleGenerationDbCapture.cs rename to Kavita.Database/Migrations/20250408222330_ScrobbleGenerationDbCapture.cs index 7431a7338..cfb63e61e 100644 --- a/API/Data/Migrations/20250408222330_ScrobbleGenerationDbCapture.cs +++ b/Kavita.Database/Migrations/20250408222330_ScrobbleGenerationDbCapture.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ScrobbleGenerationDbCapture : Migration diff --git a/API/Data/Migrations/20250415194829_KavitaPlusCBR.Designer.cs b/Kavita.Database/Migrations/20250415194829_KavitaPlusCBR.Designer.cs similarity index 99% rename from API/Data/Migrations/20250415194829_KavitaPlusCBR.Designer.cs rename to Kavita.Database/Migrations/20250415194829_KavitaPlusCBR.Designer.cs index fd287c085..c2ba1a5e6 100644 --- a/API/Data/Migrations/20250415194829_KavitaPlusCBR.Designer.cs +++ b/Kavita.Database/Migrations/20250415194829_KavitaPlusCBR.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250415194829_KavitaPlusCBR")] diff --git a/API/Data/Migrations/20250415194829_KavitaPlusCBR.cs b/Kavita.Database/Migrations/20250415194829_KavitaPlusCBR.cs similarity index 98% rename from API/Data/Migrations/20250415194829_KavitaPlusCBR.cs rename to Kavita.Database/Migrations/20250415194829_KavitaPlusCBR.cs index 188969476..0a52c1cba 100644 --- a/API/Data/Migrations/20250415194829_KavitaPlusCBR.cs +++ b/Kavita.Database/Migrations/20250415194829_KavitaPlusCBR.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class KavitaPlusCBR : Migration diff --git a/API/Data/Migrations/20250429150140_ChapterRatingAndReviews.Designer.cs b/Kavita.Database/Migrations/20250429150140_ChapterRatingAndReviews.Designer.cs similarity index 99% rename from API/Data/Migrations/20250429150140_ChapterRatingAndReviews.Designer.cs rename to Kavita.Database/Migrations/20250429150140_ChapterRatingAndReviews.Designer.cs index 52e2c4a86..75e0aa81f 100644 --- a/API/Data/Migrations/20250429150140_ChapterRatingAndReviews.Designer.cs +++ b/Kavita.Database/Migrations/20250429150140_ChapterRatingAndReviews.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250429150140_ChapterRatingAndReviews")] diff --git a/API/Data/Migrations/20250429150140_ChapterRatingAndReviews.cs b/Kavita.Database/Migrations/20250429150140_ChapterRatingAndReviews.cs similarity index 99% rename from API/Data/Migrations/20250429150140_ChapterRatingAndReviews.cs rename to Kavita.Database/Migrations/20250429150140_ChapterRatingAndReviews.cs index 5ab51aaba..6ed3f7e97 100644 --- a/API/Data/Migrations/20250429150140_ChapterRatingAndReviews.cs +++ b/Kavita.Database/Migrations/20250429150140_ChapterRatingAndReviews.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ChapterRatingAndReviews : Migration diff --git a/API/Data/Migrations/20250507221026_PersonAliases.Designer.cs b/Kavita.Database/Migrations/20250507221026_PersonAliases.Designer.cs similarity index 99% rename from API/Data/Migrations/20250507221026_PersonAliases.Designer.cs rename to Kavita.Database/Migrations/20250507221026_PersonAliases.Designer.cs index 5d76571e1..b2cc5e41f 100644 --- a/API/Data/Migrations/20250507221026_PersonAliases.Designer.cs +++ b/Kavita.Database/Migrations/20250507221026_PersonAliases.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250507221026_PersonAliases")] diff --git a/API/Data/Migrations/20250507221026_PersonAliases.cs b/Kavita.Database/Migrations/20250507221026_PersonAliases.cs similarity index 97% rename from API/Data/Migrations/20250507221026_PersonAliases.cs rename to Kavita.Database/Migrations/20250507221026_PersonAliases.cs index cb046a131..33c212b97 100644 --- a/API/Data/Migrations/20250507221026_PersonAliases.cs +++ b/Kavita.Database/Migrations/20250507221026_PersonAliases.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class PersonAliases : Migration diff --git a/API/Data/Migrations/20250519151126_KoreaderHash.Designer.cs b/Kavita.Database/Migrations/20250519151126_KoreaderHash.Designer.cs similarity index 99% rename from API/Data/Migrations/20250519151126_KoreaderHash.Designer.cs rename to Kavita.Database/Migrations/20250519151126_KoreaderHash.Designer.cs index 79f6f9504..0409544a8 100644 --- a/API/Data/Migrations/20250519151126_KoreaderHash.Designer.cs +++ b/Kavita.Database/Migrations/20250519151126_KoreaderHash.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250519151126_KoreaderHash")] diff --git a/API/Data/Migrations/20250519151126_KoreaderHash.cs b/Kavita.Database/Migrations/20250519151126_KoreaderHash.cs similarity index 94% rename from API/Data/Migrations/20250519151126_KoreaderHash.cs rename to Kavita.Database/Migrations/20250519151126_KoreaderHash.cs index 006070b72..1d3c5ee18 100644 --- a/API/Data/Migrations/20250519151126_KoreaderHash.cs +++ b/Kavita.Database/Migrations/20250519151126_KoreaderHash.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class KoreaderHash : Migration diff --git a/API/Data/Migrations/20250601200056_ReadingProfiles.Designer.cs b/Kavita.Database/Migrations/20250601200056_ReadingProfiles.Designer.cs similarity index 99% rename from API/Data/Migrations/20250601200056_ReadingProfiles.Designer.cs rename to Kavita.Database/Migrations/20250601200056_ReadingProfiles.Designer.cs index 762eae142..9f8689fd3 100644 --- a/API/Data/Migrations/20250601200056_ReadingProfiles.Designer.cs +++ b/Kavita.Database/Migrations/20250601200056_ReadingProfiles.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250601200056_ReadingProfiles")] diff --git a/API/Data/Migrations/20250601200056_ReadingProfiles.cs b/Kavita.Database/Migrations/20250601200056_ReadingProfiles.cs similarity index 99% rename from API/Data/Migrations/20250601200056_ReadingProfiles.cs rename to Kavita.Database/Migrations/20250601200056_ReadingProfiles.cs index 66b9e53e5..334bec60e 100644 --- a/API/Data/Migrations/20250601200056_ReadingProfiles.cs +++ b/Kavita.Database/Migrations/20250601200056_ReadingProfiles.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ReadingProfiles : Migration diff --git a/API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.Designer.cs b/Kavita.Database/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.Designer.cs similarity index 99% rename from API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.Designer.cs rename to Kavita.Database/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.Designer.cs index 0e9f00b4e..3b99719ad 100644 --- a/API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.Designer.cs +++ b/Kavita.Database/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint")] diff --git a/API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.cs b/Kavita.Database/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.cs similarity index 95% rename from API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.cs rename to Kavita.Database/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.cs index 11a554bdf..80840d33f 100644 --- a/API/Data/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.cs +++ b/Kavita.Database/Migrations/20250610210618_AppUserReadingProfileDisableWidthOverrideBreakPoint.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class AppUserReadingProfileDisableWidthOverrideBreakPoint : Migration diff --git a/API/Data/Migrations/20250620215058_EnableMetadataLibrary.Designer.cs b/Kavita.Database/Migrations/20250620215058_EnableMetadataLibrary.Designer.cs similarity index 99% rename from API/Data/Migrations/20250620215058_EnableMetadataLibrary.Designer.cs rename to Kavita.Database/Migrations/20250620215058_EnableMetadataLibrary.Designer.cs index c15f9f77b..3afdd6073 100644 --- a/API/Data/Migrations/20250620215058_EnableMetadataLibrary.Designer.cs +++ b/Kavita.Database/Migrations/20250620215058_EnableMetadataLibrary.Designer.cs @@ -1,6 +1,6 @@ // using System; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250620215058_EnableMetadataLibrary")] diff --git a/API/Data/Migrations/20250620215058_EnableMetadataLibrary.cs b/Kavita.Database/Migrations/20250620215058_EnableMetadataLibrary.cs similarity index 95% rename from API/Data/Migrations/20250620215058_EnableMetadataLibrary.cs rename to Kavita.Database/Migrations/20250620215058_EnableMetadataLibrary.cs index f9e38c01d..b51119aea 100644 --- a/API/Data/Migrations/20250620215058_EnableMetadataLibrary.cs +++ b/Kavita.Database/Migrations/20250620215058_EnableMetadataLibrary.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class EnableMetadataLibrary : Migration diff --git a/API/Data/Migrations/20250626162548_TrackKavitaPlusMetadata.Designer.cs b/Kavita.Database/Migrations/20250626162548_TrackKavitaPlusMetadata.Designer.cs similarity index 99% rename from API/Data/Migrations/20250626162548_TrackKavitaPlusMetadata.Designer.cs rename to Kavita.Database/Migrations/20250626162548_TrackKavitaPlusMetadata.Designer.cs index b72239924..bbb86cfd0 100644 --- a/API/Data/Migrations/20250626162548_TrackKavitaPlusMetadata.Designer.cs +++ b/Kavita.Database/Migrations/20250626162548_TrackKavitaPlusMetadata.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250626162548_TrackKavitaPlusMetadata")] diff --git a/API/Data/Migrations/20250626162548_TrackKavitaPlusMetadata.cs b/Kavita.Database/Migrations/20250626162548_TrackKavitaPlusMetadata.cs similarity index 96% rename from API/Data/Migrations/20250626162548_TrackKavitaPlusMetadata.cs rename to Kavita.Database/Migrations/20250626162548_TrackKavitaPlusMetadata.cs index ac253e0a8..80f3faf29 100644 --- a/API/Data/Migrations/20250626162548_TrackKavitaPlusMetadata.cs +++ b/Kavita.Database/Migrations/20250626162548_TrackKavitaPlusMetadata.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class TrackKavitaPlusMetadata : Migration diff --git a/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.Designer.cs b/Kavita.Database/Migrations/20250629153840_LibraryRemoveSortPrefix.Designer.cs similarity index 99% rename from API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.Designer.cs rename to Kavita.Database/Migrations/20250629153840_LibraryRemoveSortPrefix.Designer.cs index 165663f3d..3712f6e60 100644 --- a/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.Designer.cs +++ b/Kavita.Database/Migrations/20250629153840_LibraryRemoveSortPrefix.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250629153840_LibraryRemoveSortPrefix")] diff --git a/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.cs b/Kavita.Database/Migrations/20250629153840_LibraryRemoveSortPrefix.cs similarity index 95% rename from API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.cs rename to Kavita.Database/Migrations/20250629153840_LibraryRemoveSortPrefix.cs index 4800cf3fa..4d2b4ccfa 100644 --- a/API/Data/Migrations/20250629153840_LibraryRemoveSortPrefix.cs +++ b/Kavita.Database/Migrations/20250629153840_LibraryRemoveSortPrefix.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class LibraryRemoveSortPrefix : Migration diff --git a/API/Data/Migrations/20250727185204_AddEnableExtendedMetadataProcessing.Designer.cs b/Kavita.Database/Migrations/20250727185204_AddEnableExtendedMetadataProcessing.Designer.cs similarity index 99% rename from API/Data/Migrations/20250727185204_AddEnableExtendedMetadataProcessing.Designer.cs rename to Kavita.Database/Migrations/20250727185204_AddEnableExtendedMetadataProcessing.Designer.cs index fe8bfb231..5d0d16fe7 100644 --- a/API/Data/Migrations/20250727185204_AddEnableExtendedMetadataProcessing.Designer.cs +++ b/Kavita.Database/Migrations/20250727185204_AddEnableExtendedMetadataProcessing.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250727185204_AddEnableExtendedMetadataProcessing")] diff --git a/API/Data/Migrations/20250727185204_AddEnableExtendedMetadataProcessing.cs b/Kavita.Database/Migrations/20250727185204_AddEnableExtendedMetadataProcessing.cs similarity index 95% rename from API/Data/Migrations/20250727185204_AddEnableExtendedMetadataProcessing.cs rename to Kavita.Database/Migrations/20250727185204_AddEnableExtendedMetadataProcessing.cs index 6a35bcbdd..1292d1aea 100644 --- a/API/Data/Migrations/20250727185204_AddEnableExtendedMetadataProcessing.cs +++ b/Kavita.Database/Migrations/20250727185204_AddEnableExtendedMetadataProcessing.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class AddEnableExtendedMetadataProcessing : Migration diff --git a/API/Data/Migrations/20250802103258_OpenIDConnect.Designer.cs b/Kavita.Database/Migrations/20250802103258_OpenIDConnect.Designer.cs similarity index 99% rename from API/Data/Migrations/20250802103258_OpenIDConnect.Designer.cs rename to Kavita.Database/Migrations/20250802103258_OpenIDConnect.Designer.cs index e49b83a9b..0575aff14 100644 --- a/API/Data/Migrations/20250802103258_OpenIDConnect.Designer.cs +++ b/Kavita.Database/Migrations/20250802103258_OpenIDConnect.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250802103258_OpenIDConnect")] diff --git a/API/Data/Migrations/20250802103258_OpenIDConnect.cs b/Kavita.Database/Migrations/20250802103258_OpenIDConnect.cs similarity index 96% rename from API/Data/Migrations/20250802103258_OpenIDConnect.cs rename to Kavita.Database/Migrations/20250802103258_OpenIDConnect.cs index 0bad34851..37ceb58ce 100644 --- a/API/Data/Migrations/20250802103258_OpenIDConnect.cs +++ b/Kavita.Database/Migrations/20250802103258_OpenIDConnect.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class OpenIDConnect : Migration diff --git a/API/Data/Migrations/20250820150458_BookAnnotations.Designer.cs b/Kavita.Database/Migrations/20250820150458_BookAnnotations.Designer.cs similarity index 99% rename from API/Data/Migrations/20250820150458_BookAnnotations.Designer.cs rename to Kavita.Database/Migrations/20250820150458_BookAnnotations.Designer.cs index a8822d149..9a8e5ae43 100644 --- a/API/Data/Migrations/20250820150458_BookAnnotations.Designer.cs +++ b/Kavita.Database/Migrations/20250820150458_BookAnnotations.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250820150458_BookAnnotations")] diff --git a/API/Data/Migrations/20250820150458_BookAnnotations.cs b/Kavita.Database/Migrations/20250820150458_BookAnnotations.cs similarity index 99% rename from API/Data/Migrations/20250820150458_BookAnnotations.cs rename to Kavita.Database/Migrations/20250820150458_BookAnnotations.cs index ac0e88f8e..315690627 100644 --- a/API/Data/Migrations/20250820150458_BookAnnotations.cs +++ b/Kavita.Database/Migrations/20250820150458_BookAnnotations.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class BookAnnotations : Migration diff --git a/API/Data/Migrations/20250919114119_ColorScapeSetting.Designer.cs b/Kavita.Database/Migrations/20250919114119_ColorScapeSetting.Designer.cs similarity index 99% rename from API/Data/Migrations/20250919114119_ColorScapeSetting.Designer.cs rename to Kavita.Database/Migrations/20250919114119_ColorScapeSetting.Designer.cs index 67c146f48..2e0e04e7c 100644 --- a/API/Data/Migrations/20250919114119_ColorScapeSetting.Designer.cs +++ b/Kavita.Database/Migrations/20250919114119_ColorScapeSetting.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250919114119_ColorScapeSetting")] diff --git a/API/Data/Migrations/20250919114119_ColorScapeSetting.cs b/Kavita.Database/Migrations/20250919114119_ColorScapeSetting.cs similarity index 95% rename from API/Data/Migrations/20250919114119_ColorScapeSetting.cs rename to Kavita.Database/Migrations/20250919114119_ColorScapeSetting.cs index 445ada7a3..4e12d320d 100644 --- a/API/Data/Migrations/20250919114119_ColorScapeSetting.cs +++ b/Kavita.Database/Migrations/20250919114119_ColorScapeSetting.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ColorScapeSetting : Migration diff --git a/API/Data/Migrations/20250920212509_CustomEpubFonts.Designer.cs b/Kavita.Database/Migrations/20250920212509_CustomEpubFonts.Designer.cs similarity index 99% rename from API/Data/Migrations/20250920212509_CustomEpubFonts.Designer.cs rename to Kavita.Database/Migrations/20250920212509_CustomEpubFonts.Designer.cs index 5ce969a9a..e83b94038 100644 --- a/API/Data/Migrations/20250920212509_CustomEpubFonts.Designer.cs +++ b/Kavita.Database/Migrations/20250920212509_CustomEpubFonts.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250920212509_CustomEpubFonts")] diff --git a/API/Data/Migrations/20250920212509_CustomEpubFonts.cs b/Kavita.Database/Migrations/20250920212509_CustomEpubFonts.cs similarity index 97% rename from API/Data/Migrations/20250920212509_CustomEpubFonts.cs rename to Kavita.Database/Migrations/20250920212509_CustomEpubFonts.cs index 1a8505b52..ddd5f87bd 100644 --- a/API/Data/Migrations/20250920212509_CustomEpubFonts.cs +++ b/Kavita.Database/Migrations/20250920212509_CustomEpubFonts.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class CustomEpubFonts : Migration diff --git a/API/Data/Migrations/20250921211542_EpubPageCalcMethod.Designer.cs b/Kavita.Database/Migrations/20250921211542_EpubPageCalcMethod.Designer.cs similarity index 99% rename from API/Data/Migrations/20250921211542_EpubPageCalcMethod.Designer.cs rename to Kavita.Database/Migrations/20250921211542_EpubPageCalcMethod.Designer.cs index 0c1db0f09..2b9b24e58 100644 --- a/API/Data/Migrations/20250921211542_EpubPageCalcMethod.Designer.cs +++ b/Kavita.Database/Migrations/20250921211542_EpubPageCalcMethod.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250921211542_EpubPageCalcMethod")] diff --git a/API/Data/Migrations/20250921211542_EpubPageCalcMethod.cs b/Kavita.Database/Migrations/20250921211542_EpubPageCalcMethod.cs similarity index 95% rename from API/Data/Migrations/20250921211542_EpubPageCalcMethod.cs rename to Kavita.Database/Migrations/20250921211542_EpubPageCalcMethod.cs index d6a8b02c0..54bc0fb74 100644 --- a/API/Data/Migrations/20250921211542_EpubPageCalcMethod.cs +++ b/Kavita.Database/Migrations/20250921211542_EpubPageCalcMethod.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class EpubPageCalcMethod : Migration diff --git a/API/Data/Migrations/20250924142016_AddAnnotationsHtmlContent.Designer.cs b/Kavita.Database/Migrations/20250924142016_AddAnnotationsHtmlContent.Designer.cs similarity index 99% rename from API/Data/Migrations/20250924142016_AddAnnotationsHtmlContent.Designer.cs rename to Kavita.Database/Migrations/20250924142016_AddAnnotationsHtmlContent.Designer.cs index d239e558d..1a3839910 100644 --- a/API/Data/Migrations/20250924142016_AddAnnotationsHtmlContent.Designer.cs +++ b/Kavita.Database/Migrations/20250924142016_AddAnnotationsHtmlContent.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250924142016_AddAnnotationsHtmlContent")] diff --git a/API/Data/Migrations/20250924142016_AddAnnotationsHtmlContent.cs b/Kavita.Database/Migrations/20250924142016_AddAnnotationsHtmlContent.cs similarity index 98% rename from API/Data/Migrations/20250924142016_AddAnnotationsHtmlContent.cs rename to Kavita.Database/Migrations/20250924142016_AddAnnotationsHtmlContent.cs index 620e37d03..8effff21e 100644 --- a/API/Data/Migrations/20250924142016_AddAnnotationsHtmlContent.cs +++ b/Kavita.Database/Migrations/20250924142016_AddAnnotationsHtmlContent.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class AddAnnotationsHtmlContent : Migration diff --git a/API/Data/Migrations/20250928181727_RemoveEpubPageCalc.Designer.cs b/Kavita.Database/Migrations/20250928181727_RemoveEpubPageCalc.Designer.cs similarity index 99% rename from API/Data/Migrations/20250928181727_RemoveEpubPageCalc.Designer.cs rename to Kavita.Database/Migrations/20250928181727_RemoveEpubPageCalc.Designer.cs index 437890075..dac2eb8ba 100644 --- a/API/Data/Migrations/20250928181727_RemoveEpubPageCalc.Designer.cs +++ b/Kavita.Database/Migrations/20250928181727_RemoveEpubPageCalc.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20250928181727_RemoveEpubPageCalc")] diff --git a/API/Data/Migrations/20250928181727_RemoveEpubPageCalc.cs b/Kavita.Database/Migrations/20250928181727_RemoveEpubPageCalc.cs similarity index 95% rename from API/Data/Migrations/20250928181727_RemoveEpubPageCalc.cs rename to Kavita.Database/Migrations/20250928181727_RemoveEpubPageCalc.cs index 8074f1fc5..30e65ffb1 100644 --- a/API/Data/Migrations/20250928181727_RemoveEpubPageCalc.cs +++ b/Kavita.Database/Migrations/20250928181727_RemoveEpubPageCalc.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class RemoveEpubPageCalc : Migration diff --git a/API/Data/Migrations/20251003110154_SocialAnnotations.Designer.cs b/Kavita.Database/Migrations/20251003110154_SocialAnnotations.Designer.cs similarity index 99% rename from API/Data/Migrations/20251003110154_SocialAnnotations.Designer.cs rename to Kavita.Database/Migrations/20251003110154_SocialAnnotations.Designer.cs index 687585e5c..cb0fd84ea 100644 --- a/API/Data/Migrations/20251003110154_SocialAnnotations.Designer.cs +++ b/Kavita.Database/Migrations/20251003110154_SocialAnnotations.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20251003110154_SocialAnnotations")] diff --git a/API/Data/Migrations/20251003110154_SocialAnnotations.cs b/Kavita.Database/Migrations/20251003110154_SocialAnnotations.cs similarity index 98% rename from API/Data/Migrations/20251003110154_SocialAnnotations.cs rename to Kavita.Database/Migrations/20251003110154_SocialAnnotations.cs index ecd522211..f14ed4937 100644 --- a/API/Data/Migrations/20251003110154_SocialAnnotations.cs +++ b/Kavita.Database/Migrations/20251003110154_SocialAnnotations.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class SocialAnnotations : Migration diff --git a/API/Data/Migrations/20251009150922_DataSaverUserSetting.Designer.cs b/Kavita.Database/Migrations/20251009150922_DataSaverUserSetting.Designer.cs similarity index 99% rename from API/Data/Migrations/20251009150922_DataSaverUserSetting.Designer.cs rename to Kavita.Database/Migrations/20251009150922_DataSaverUserSetting.Designer.cs index 690f5152d..d6508c583 100644 --- a/API/Data/Migrations/20251009150922_DataSaverUserSetting.Designer.cs +++ b/Kavita.Database/Migrations/20251009150922_DataSaverUserSetting.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20251009150922_DataSaverUserSetting")] diff --git a/API/Data/Migrations/20251009150922_DataSaverUserSetting.cs b/Kavita.Database/Migrations/20251009150922_DataSaverUserSetting.cs similarity index 95% rename from API/Data/Migrations/20251009150922_DataSaverUserSetting.cs rename to Kavita.Database/Migrations/20251009150922_DataSaverUserSetting.cs index 0a744dfc7..0c5484637 100644 --- a/API/Data/Migrations/20251009150922_DataSaverUserSetting.cs +++ b/Kavita.Database/Migrations/20251009150922_DataSaverUserSetting.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class DataSaverUserSetting : Migration diff --git a/API/Data/Migrations/20251023205956_SeriesInheritWebLinksFromFirstChapter.Designer.cs b/Kavita.Database/Migrations/20251023205956_SeriesInheritWebLinksFromFirstChapter.Designer.cs similarity index 99% rename from API/Data/Migrations/20251023205956_SeriesInheritWebLinksFromFirstChapter.Designer.cs rename to Kavita.Database/Migrations/20251023205956_SeriesInheritWebLinksFromFirstChapter.Designer.cs index 81b85a6f7..53dbdf0bb 100644 --- a/API/Data/Migrations/20251023205956_SeriesInheritWebLinksFromFirstChapter.Designer.cs +++ b/Kavita.Database/Migrations/20251023205956_SeriesInheritWebLinksFromFirstChapter.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20251023205956_SeriesInheritWebLinksFromFirstChapter")] diff --git a/API/Data/Migrations/20251023205956_SeriesInheritWebLinksFromFirstChapter.cs b/Kavita.Database/Migrations/20251023205956_SeriesInheritWebLinksFromFirstChapter.cs similarity index 95% rename from API/Data/Migrations/20251023205956_SeriesInheritWebLinksFromFirstChapter.cs rename to Kavita.Database/Migrations/20251023205956_SeriesInheritWebLinksFromFirstChapter.cs index 0cfae8900..e57d1aff5 100644 --- a/API/Data/Migrations/20251023205956_SeriesInheritWebLinksFromFirstChapter.cs +++ b/Kavita.Database/Migrations/20251023205956_SeriesInheritWebLinksFromFirstChapter.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class SeriesInheritWebLinksFromFirstChapter : Migration diff --git a/API/Data/Migrations/20251026234845_LibraryDefaultLanguageCustomKeyBinds.Designer.cs b/Kavita.Database/Migrations/20251026234845_LibraryDefaultLanguageCustomKeyBinds.Designer.cs similarity index 99% rename from API/Data/Migrations/20251026234845_LibraryDefaultLanguageCustomKeyBinds.Designer.cs rename to Kavita.Database/Migrations/20251026234845_LibraryDefaultLanguageCustomKeyBinds.Designer.cs index 7fab60ea3..eff6f70f5 100644 --- a/API/Data/Migrations/20251026234845_LibraryDefaultLanguageCustomKeyBinds.Designer.cs +++ b/Kavita.Database/Migrations/20251026234845_LibraryDefaultLanguageCustomKeyBinds.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20251026234845_LibraryDefaultLanguageCustomKeyBinds")] diff --git a/API/Data/Migrations/20251026234845_LibraryDefaultLanguageCustomKeyBinds.cs b/Kavita.Database/Migrations/20251026234845_LibraryDefaultLanguageCustomKeyBinds.cs similarity index 96% rename from API/Data/Migrations/20251026234845_LibraryDefaultLanguageCustomKeyBinds.cs rename to Kavita.Database/Migrations/20251026234845_LibraryDefaultLanguageCustomKeyBinds.cs index e1ad21e18..7d323ae37 100644 --- a/API/Data/Migrations/20251026234845_LibraryDefaultLanguageCustomKeyBinds.cs +++ b/Kavita.Database/Migrations/20251026234845_LibraryDefaultLanguageCustomKeyBinds.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class LibraryDefaultLanguageCustomKeyBinds : Migration diff --git a/API/Data/Migrations/20251101152738_OpdsSettings.Designer.cs b/Kavita.Database/Migrations/20251101152738_OpdsSettings.Designer.cs similarity index 99% rename from API/Data/Migrations/20251101152738_OpdsSettings.Designer.cs rename to Kavita.Database/Migrations/20251101152738_OpdsSettings.Designer.cs index 2ec446aba..7bc381e3d 100644 --- a/API/Data/Migrations/20251101152738_OpdsSettings.Designer.cs +++ b/Kavita.Database/Migrations/20251101152738_OpdsSettings.Designer.cs @@ -1,8 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -10,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20251101152738_OpdsSettings")] diff --git a/API/Data/Migrations/20251101152738_OpdsSettings.cs b/Kavita.Database/Migrations/20251101152738_OpdsSettings.cs similarity index 95% rename from API/Data/Migrations/20251101152738_OpdsSettings.cs rename to Kavita.Database/Migrations/20251101152738_OpdsSettings.cs index b40b1991d..45220bee9 100644 --- a/API/Data/Migrations/20251101152738_OpdsSettings.cs +++ b/Kavita.Database/Migrations/20251101152738_OpdsSettings.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class OpdsSettings : Migration diff --git a/API/Data/Migrations/20251207204514_StatsRevampPartOne.Designer.cs b/Kavita.Database/Migrations/20251207204514_StatsRevampPartOne.Designer.cs similarity index 99% rename from API/Data/Migrations/20251207204514_StatsRevampPartOne.Designer.cs rename to Kavita.Database/Migrations/20251207204514_StatsRevampPartOne.Designer.cs index ac9d1ff08..17a221003 100644 --- a/API/Data/Migrations/20251207204514_StatsRevampPartOne.Designer.cs +++ b/Kavita.Database/Migrations/20251207204514_StatsRevampPartOne.Designer.cs @@ -1,9 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; -using API.Entities.Progress; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -11,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20251207204514_StatsRevampPartOne")] diff --git a/API/Data/Migrations/20251207204514_StatsRevampPartOne.cs b/Kavita.Database/Migrations/20251207204514_StatsRevampPartOne.cs similarity index 99% rename from API/Data/Migrations/20251207204514_StatsRevampPartOne.cs rename to Kavita.Database/Migrations/20251207204514_StatsRevampPartOne.cs index c95f34e71..287901513 100644 --- a/API/Data/Migrations/20251207204514_StatsRevampPartOne.cs +++ b/Kavita.Database/Migrations/20251207204514_StatsRevampPartOne.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class StatsRevampPartOne : Migration diff --git a/API/Data/Migrations/20251210145923_BookmarkRelationshipAndSearchIndex.Designer.cs b/Kavita.Database/Migrations/20251210145923_BookmarkRelationshipAndSearchIndex.Designer.cs similarity index 99% rename from API/Data/Migrations/20251210145923_BookmarkRelationshipAndSearchIndex.Designer.cs rename to Kavita.Database/Migrations/20251210145923_BookmarkRelationshipAndSearchIndex.Designer.cs index 8f4cfdfdf..810dd9ced 100644 --- a/API/Data/Migrations/20251210145923_BookmarkRelationshipAndSearchIndex.Designer.cs +++ b/Kavita.Database/Migrations/20251210145923_BookmarkRelationshipAndSearchIndex.Designer.cs @@ -1,9 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; -using API.Entities.Progress; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -11,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20251210145923_BookmarkRelationshipAndSearchIndex")] diff --git a/API/Data/Migrations/20251210145923_BookmarkRelationshipAndSearchIndex.cs b/Kavita.Database/Migrations/20251210145923_BookmarkRelationshipAndSearchIndex.cs similarity index 99% rename from API/Data/Migrations/20251210145923_BookmarkRelationshipAndSearchIndex.cs rename to Kavita.Database/Migrations/20251210145923_BookmarkRelationshipAndSearchIndex.cs index 93cf79fbf..72f5dcac0 100644 --- a/API/Data/Migrations/20251210145923_BookmarkRelationshipAndSearchIndex.cs +++ b/Kavita.Database/Migrations/20251210145923_BookmarkRelationshipAndSearchIndex.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class BookmarkRelationshipAndSearchIndex : Migration diff --git a/API/Data/Migrations/20251218200802_ReadingSessionFormatAndIndecies.Designer.cs b/Kavita.Database/Migrations/20251218200802_ReadingSessionFormatAndIndecies.Designer.cs similarity index 99% rename from API/Data/Migrations/20251218200802_ReadingSessionFormatAndIndecies.Designer.cs rename to Kavita.Database/Migrations/20251218200802_ReadingSessionFormatAndIndecies.Designer.cs index 5f5a9e133..f174db443 100644 --- a/API/Data/Migrations/20251218200802_ReadingSessionFormatAndIndecies.Designer.cs +++ b/Kavita.Database/Migrations/20251218200802_ReadingSessionFormatAndIndecies.Designer.cs @@ -1,9 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; -using API.Entities.Progress; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -11,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20251218200802_ReadingSessionFormatAndIndecies")] diff --git a/API/Data/Migrations/20251218200802_ReadingSessionFormatAndIndecies.cs b/Kavita.Database/Migrations/20251218200802_ReadingSessionFormatAndIndecies.cs similarity index 98% rename from API/Data/Migrations/20251218200802_ReadingSessionFormatAndIndecies.cs rename to Kavita.Database/Migrations/20251218200802_ReadingSessionFormatAndIndecies.cs index a9509e1ce..84815ce42 100644 --- a/API/Data/Migrations/20251218200802_ReadingSessionFormatAndIndecies.cs +++ b/Kavita.Database/Migrations/20251218200802_ReadingSessionFormatAndIndecies.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ReadingSessionFormatAndIndecies : Migration diff --git a/API/Data/Migrations/20251224133055_AddDataProtectionKeys.Designer.cs b/Kavita.Database/Migrations/20251224133055_AddDataProtectionKeys.Designer.cs similarity index 99% rename from API/Data/Migrations/20251224133055_AddDataProtectionKeys.Designer.cs rename to Kavita.Database/Migrations/20251224133055_AddDataProtectionKeys.Designer.cs index f04ab9609..a765111ba 100644 --- a/API/Data/Migrations/20251224133055_AddDataProtectionKeys.Designer.cs +++ b/Kavita.Database/Migrations/20251224133055_AddDataProtectionKeys.Designer.cs @@ -1,9 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; -using API.Entities.Progress; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -11,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20251224133055_AddDataProtectionKeys")] diff --git a/API/Data/Migrations/20251224133055_AddDataProtectionKeys.cs b/Kavita.Database/Migrations/20251224133055_AddDataProtectionKeys.cs similarity index 96% rename from API/Data/Migrations/20251224133055_AddDataProtectionKeys.cs rename to Kavita.Database/Migrations/20251224133055_AddDataProtectionKeys.cs index 19956c5f9..8948df86f 100644 --- a/API/Data/Migrations/20251224133055_AddDataProtectionKeys.cs +++ b/Kavita.Database/Migrations/20251224133055_AddDataProtectionKeys.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class AddDataProtectionKeys : Migration diff --git a/API/Data/Migrations/20251229144718_AddDeviceIdsToReadingProfiles.Designer.cs b/Kavita.Database/Migrations/20251229144718_AddDeviceIdsToReadingProfiles.Designer.cs similarity index 99% rename from API/Data/Migrations/20251229144718_AddDeviceIdsToReadingProfiles.Designer.cs rename to Kavita.Database/Migrations/20251229144718_AddDeviceIdsToReadingProfiles.Designer.cs index a3a42e2f1..a521b41cf 100644 --- a/API/Data/Migrations/20251229144718_AddDeviceIdsToReadingProfiles.Designer.cs +++ b/Kavita.Database/Migrations/20251229144718_AddDeviceIdsToReadingProfiles.Designer.cs @@ -1,9 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; -using API.Entities.Progress; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -11,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20251229144718_AddDeviceIdsToReadingProfiles")] diff --git a/API/Data/Migrations/20251229144718_AddDeviceIdsToReadingProfiles.cs b/Kavita.Database/Migrations/20251229144718_AddDeviceIdsToReadingProfiles.cs similarity index 98% rename from API/Data/Migrations/20251229144718_AddDeviceIdsToReadingProfiles.cs rename to Kavita.Database/Migrations/20251229144718_AddDeviceIdsToReadingProfiles.cs index 80df7309e..fbc94b5f3 100644 --- a/API/Data/Migrations/20251229144718_AddDeviceIdsToReadingProfiles.cs +++ b/Kavita.Database/Migrations/20251229144718_AddDeviceIdsToReadingProfiles.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class AddDeviceIdsToReadingProfiles : Migration diff --git a/API/Data/Migrations/20260109144351_ReadingSessionIndex.Designer.cs b/Kavita.Database/Migrations/20260109144351_ReadingSessionIndex.Designer.cs similarity index 99% rename from API/Data/Migrations/20260109144351_ReadingSessionIndex.Designer.cs rename to Kavita.Database/Migrations/20260109144351_ReadingSessionIndex.Designer.cs index d38a7c19d..1994f26f6 100644 --- a/API/Data/Migrations/20260109144351_ReadingSessionIndex.Designer.cs +++ b/Kavita.Database/Migrations/20260109144351_ReadingSessionIndex.Designer.cs @@ -1,9 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; -using API.Entities.Progress; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -11,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20260109144351_ReadingSessionIndex")] diff --git a/API/Data/Migrations/20260109144351_ReadingSessionIndex.cs b/Kavita.Database/Migrations/20260109144351_ReadingSessionIndex.cs similarity index 97% rename from API/Data/Migrations/20260109144351_ReadingSessionIndex.cs rename to Kavita.Database/Migrations/20260109144351_ReadingSessionIndex.cs index e7dcbf04f..50280a630 100644 --- a/API/Data/Migrations/20260109144351_ReadingSessionIndex.cs +++ b/Kavita.Database/Migrations/20260109144351_ReadingSessionIndex.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ReadingSessionIndex : Migration diff --git a/API/Data/Migrations/20260110164419_AppUserAuthKeyUtcMissing.Designer.cs b/Kavita.Database/Migrations/20260110164419_AppUserAuthKeyUtcMissing.Designer.cs similarity index 99% rename from API/Data/Migrations/20260110164419_AppUserAuthKeyUtcMissing.Designer.cs rename to Kavita.Database/Migrations/20260110164419_AppUserAuthKeyUtcMissing.Designer.cs index 302d41386..df0223a7b 100644 --- a/API/Data/Migrations/20260110164419_AppUserAuthKeyUtcMissing.Designer.cs +++ b/Kavita.Database/Migrations/20260110164419_AppUserAuthKeyUtcMissing.Designer.cs @@ -1,9 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; -using API.Entities.Progress; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -11,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20260110164419_AppUserAuthKeyUtcMissing")] diff --git a/API/Data/Migrations/20260110164419_AppUserAuthKeyUtcMissing.cs b/Kavita.Database/Migrations/20260110164419_AppUserAuthKeyUtcMissing.cs similarity index 95% rename from API/Data/Migrations/20260110164419_AppUserAuthKeyUtcMissing.cs rename to Kavita.Database/Migrations/20260110164419_AppUserAuthKeyUtcMissing.cs index f3fdb69cd..09ff54ca0 100644 --- a/API/Data/Migrations/20260110164419_AppUserAuthKeyUtcMissing.cs +++ b/Kavita.Database/Migrations/20260110164419_AppUserAuthKeyUtcMissing.cs @@ -2,7 +2,7 @@ #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class AppUserAuthKeyUtcMissing : Migration diff --git a/API/Data/Migrations/20260112165908_ReadingHistoryChanges.Designer.cs b/Kavita.Database/Migrations/20260112165908_ReadingHistoryChanges.Designer.cs similarity index 99% rename from API/Data/Migrations/20260112165908_ReadingHistoryChanges.Designer.cs rename to Kavita.Database/Migrations/20260112165908_ReadingHistoryChanges.Designer.cs index 988d3823b..e98766293 100644 --- a/API/Data/Migrations/20260112165908_ReadingHistoryChanges.Designer.cs +++ b/Kavita.Database/Migrations/20260112165908_ReadingHistoryChanges.Designer.cs @@ -1,9 +1,7 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; -using API.Entities.Progress; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -11,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] [Migration("20260112165908_ReadingHistoryChanges")] diff --git a/API/Data/Migrations/20260112165908_ReadingHistoryChanges.cs b/Kavita.Database/Migrations/20260112165908_ReadingHistoryChanges.cs similarity index 98% rename from API/Data/Migrations/20260112165908_ReadingHistoryChanges.cs rename to Kavita.Database/Migrations/20260112165908_ReadingHistoryChanges.cs index e22cb1a61..53214c013 100644 --- a/API/Data/Migrations/20260112165908_ReadingHistoryChanges.cs +++ b/Kavita.Database/Migrations/20260112165908_ReadingHistoryChanges.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { /// public partial class ReadingHistoryChanges : Migration diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/Kavita.Database/Migrations/DataContextModelSnapshot.cs similarity index 99% rename from API/Data/Migrations/DataContextModelSnapshot.cs rename to Kavita.Database/Migrations/DataContextModelSnapshot.cs index 6c8d15a19..f8049cfb0 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/Kavita.Database/Migrations/DataContextModelSnapshot.cs @@ -1,16 +1,14 @@ // using System; using System.Collections.Generic; -using API.Data; -using API.Entities.MetadataMatching; -using API.Entities.Progress; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace API.Data.Migrations +namespace Kavita.Database.Migrations { [DbContext(typeof(DataContext))] partial class DataContextModelSnapshot : ModelSnapshot diff --git a/API/Data/Repositories/AnnotationRepository.cs b/Kavita.Database/Repositories/AnnotationRepository.cs similarity index 78% rename from API/Data/Repositories/AnnotationRepository.cs rename to Kavita.Database/Repositories/AnnotationRepository.cs index a2b69a39f..7a947e78f 100644 --- a/API/Data/Repositories/AnnotationRepository.cs +++ b/Kavita.Database/Repositories/AnnotationRepository.cs @@ -1,39 +1,25 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.DTOs; -using API.DTOs.Filtering.v2; -using API.DTOs.Metadata.Browse.Requests; -using API.DTOs.Annotations; -using API.DTOs.Reader; -using API.Entities; -using API.Extensions.QueryExtensions; -using API.Extensions.QueryExtensions.Filtering; -using API.Helpers; -using API.Helpers.Converters; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Common.Helpers; +using Kavita.Database.Converters; +using Kavita.Database.Extensions; +using Kavita.Database.Extensions.Filters; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Annotations; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.DTOs.Metadata.Browse.Requests; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; -#nullable enable +namespace Kavita.Database.Repositories; -public interface IAnnotationRepository -{ - void Attach(AppUserAnnotation annotation); - void Update(AppUserAnnotation annotation); - void Remove(AppUserAnnotation annotation); - void Remove(IEnumerable annotations); - Task GetAnnotationDto(int id); - Task GetAnnotation(int id); - Task> GetAllAnnotations(); - Task> GetAnnotations(int userId, IList ids); - Task> GetFullAnnotationsByUserIdAsync(int userId); - Task> GetFullAnnotations(int userId, IList annotationIds); - Task> GetAnnotationDtos(int userId, BrowseAnnotationFilterDto filter, UserParams userParams); - Task> GetSeriesWithAnnotations(int userId); -} public class AnnotationRepository(DataContext context, IMapper mapper) : IAnnotationRepository { @@ -57,43 +43,44 @@ public class AnnotationRepository(DataContext context, IMapper mapper) : IAnnota context.AppUserAnnotation.RemoveRange(annotations); } - public async Task GetAnnotationDto(int id) + public async Task GetAnnotationDto(int id, CancellationToken ct = default) { return await context.AppUserAnnotation .ProjectTo(mapper.ConfigurationProvider) - .FirstOrDefaultAsync(a => a.Id == id); + .FirstOrDefaultAsync(a => a.Id == id, ct); } - public async Task GetAnnotation(int id) + public async Task GetAnnotation(int id, CancellationToken ct = default) { return await context.AppUserAnnotation - .FirstOrDefaultAsync(a => a.Id == id); + .FirstOrDefaultAsync(a => a.Id == id, ct); } - public async Task> GetAllAnnotations() + public async Task> GetAllAnnotations(CancellationToken ct = default) { - return await context.AppUserAnnotation.ToListAsync(); + return await context.AppUserAnnotation.ToListAsync(ct); } - public async Task> GetAnnotations(int userId, IList ids) + public async Task> GetAnnotations(int userId, IList ids, CancellationToken ct = default) { - var userPreferences = await context.AppUserPreferences.ToListAsync(); + var userPreferences = await context.AppUserPreferences.ToListAsync(ct); return await context.AppUserAnnotation .Where(a => ids.Contains(a.Id)) .RestrictBySocialPreferences(userId, userPreferences) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetAnnotationDtos(int userId, BrowseAnnotationFilterDto filter, UserParams userParams) + public async Task> GetAnnotationDtos(int userId, BrowseAnnotationFilterDto filter, + UserParams userParams, CancellationToken ct = default) { var query = await CreatedFilteredAnnotationQueryable(userId, filter); - return await PagedList.CreateAsync(query, userParams); + return await PagedList.CreateAsync(query, userParams, ct); } - public async Task> GetSeriesWithAnnotations(int userId) + public async Task> GetSeriesWithAnnotations(int userId, CancellationToken ct = default) { - var userPreferences = await context.AppUserPreferences.ToListAsync(); + var userPreferences = await context.AppUserPreferences.ToListAsync(ct); var libraryIds = context.AppUser.GetLibraryIdsForUser(userId); var userRating = await context.AppUser.GetUserAgeRestriction(userId); @@ -101,13 +88,13 @@ public class AnnotationRepository(DataContext context, IMapper mapper) : IAnnota var seriesIdsWithAnnotations = await context.AppUserAnnotation .RestrictBySocialPreferences(userId, userPreferences) .Select(a => a.SeriesId) - .ToListAsync(); + .ToListAsync(ct); return await context.Series .Where(s => libraryIds.Contains(s.LibraryId) && seriesIdsWithAnnotations.Contains(s.Id)) .RestrictAgainstAgeRestriction(userRating) .ProjectTo(mapper.ConfigurationProvider) - .ToListAsync(); + .ToListAsync(ct); } @@ -180,9 +167,10 @@ public class AnnotationRepository(DataContext context, IMapper mapper) : IAnnota }; } - public async Task> GetFullAnnotations(int userId, IList annotationIds) + public async Task> GetFullAnnotations(int userId, IList annotationIds, + CancellationToken ct = default) { - var userPreferences = await context.AppUserPreferences.ToListAsync(); + var userPreferences = await context.AppUserPreferences.ToListAsync(ct); return await context.AppUserAnnotation .AsNoTracking() @@ -190,22 +178,23 @@ public class AnnotationRepository(DataContext context, IMapper mapper) : IAnnota .RestrictBySocialPreferences(userId, userPreferences) .ProjectTo(mapper.ConfigurationProvider) .OrderFullAnnotation() - .ToListAsync(); + .ToListAsync(ct); } /// /// This does not track! /// /// + /// /// - public async Task> GetFullAnnotationsByUserIdAsync(int userId) + public async Task> GetFullAnnotationsByUserIdAsync(int userId, CancellationToken ct = default) { - var userPreferences = await context.AppUserPreferences.ToListAsync(); + var userPreferences = await context.AppUserPreferences.ToListAsync(ct); return await context.AppUserAnnotation .RestrictBySocialPreferences(userId, userPreferences) .ProjectTo(mapper.ConfigurationProvider) .OrderFullAnnotation() - .ToListAsync(); + .ToListAsync(ct); } } diff --git a/API/Data/Repositories/AppUserExternalSourceRepository.cs b/Kavita.Database/Repositories/AppUserExternalSourceRepository.cs similarity index 50% rename from API/Data/Repositories/AppUserExternalSourceRepository.cs rename to Kavita.Database/Repositories/AppUserExternalSourceRepository.cs index 60f335599..9e3f4327f 100644 --- a/API/Data/Repositories/AppUserExternalSourceRepository.cs +++ b/Kavita.Database/Repositories/AppUserExternalSourceRepository.cs @@ -1,77 +1,65 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.DTOs.SideNav; -using API.Entities; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; using Kavita.Common.Helpers; +using Kavita.Models.DTOs.SideNav; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; +namespace Kavita.Database.Repositories; -public interface IAppUserExternalSourceRepository + +public class AppUserExternalSourceRepository(DataContext context, IMapper mapper) : IAppUserExternalSourceRepository { - void Update(AppUserExternalSource source); - void Delete(AppUserExternalSource source); - Task GetById(int externalSourceId); - Task> GetExternalSources(int userId); - Task ExternalSourceExists(int userId, string name, string host, string apiKey); -} - -public class AppUserExternalSourceRepository : IAppUserExternalSourceRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public AppUserExternalSourceRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } public void Update(AppUserExternalSource source) { - _context.Entry(source).State = EntityState.Modified; + context.AppUserExternalSource.Update(source); } public void Delete(AppUserExternalSource source) { - _context.AppUserExternalSource.Remove(source); + context.AppUserExternalSource.Remove(source); } - public async Task GetById(int externalSourceId) + public async Task GetById(int externalSourceId, CancellationToken ct = default) { - return await _context.AppUserExternalSource + return await context.AppUserExternalSource .Where(s => s.Id == externalSourceId) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task> GetExternalSources(int userId) + public async Task> GetExternalSources(int userId, CancellationToken ct = default) { - return await _context.AppUserExternalSource.Where(s => s.AppUserId == userId) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + return await context.AppUserExternalSource.Where(s => s.AppUserId == userId) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } /// /// Checks if all the properties match exactly. This will allow a user to setup 2 External Sources with different Users /// /// - /// /// + /// /// + /// /// - public async Task ExternalSourceExists(int userId, string name, string host, string apiKey) + public async Task ExternalSourceExists(int userId, string name, string host, string apiKey, + CancellationToken ct = default) { host = host.Trim(); if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(name) || string.IsNullOrEmpty(apiKey)) return false; var hostWithEndingSlash = UrlHelper.EnsureEndsWithSlash(host)!; - return await _context.AppUserExternalSource + return await context.AppUserExternalSource .Where(s => s.AppUserId == userId ) .Where(s => s.Host.ToUpper().Equals(hostWithEndingSlash.ToUpper()) && s.Name.ToUpper().Equals(name.ToUpper()) && s.ApiKey.Equals(apiKey)) - .AnyAsync(); + .AnyAsync(ct); } } diff --git a/API/Data/Repositories/AppUserProgressRepository.cs b/Kavita.Database/Repositories/AppUserProgressRepository.cs similarity index 55% rename from API/Data/Repositories/AppUserProgressRepository.cs rename to Kavita.Database/Repositories/AppUserProgressRepository.cs index 24b6cc0e9..e2d6f9549 100644 --- a/API/Data/Repositories/AppUserProgressRepository.cs +++ b/Kavita.Database/Repositories/AppUserProgressRepository.cs @@ -2,116 +2,87 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; -using API.DTOs.Progress; -using API.Entities.Enums; -using API.Entities.Progress; -using API.Extensions.QueryExtensions; -using API.Services.Tasks.Scanner.Parser; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Common.Constants; +using Kavita.Database.Extensions; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; -#nullable enable +namespace Kavita.Database.Repositories; -public interface IAppUserProgressRepository +public class AppUserProgressRepository(DataContext context, IMapper mapper) : IAppUserProgressRepository { - void Update(AppUserProgress userProgress); - void Remove(AppUserProgress userProgress); - Task CleanupAbandonedChapters(); - Task UserHasProgress(LibraryType libraryType, int userId); - Task GetUserProgressAsync(int chapterId, int userId); - Task HasAnyProgressOnSeriesAsync(int seriesId, int userId); - Task> GetUserProgressForSeriesAsync(int seriesId, int userId); - Task> GetAllProgress(); - Task GetLatestProgress(); - 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 GetLatestProgressForSeries(int seriesId, int userId); - Task GetLatestProgressForVolume(int volumeId, int userId); - Task GetLatestProgressForChapter(int chapterId, int userId); - Task GetFirstProgressForSeries(int seriesId, int userId); - Task GetFirstProgressForUser(int userId); - Task UpdateAllProgressThatAreMoreThanChapterPages(); - Task> GetUserProgressForChapter(int chapterId, int userId = 0); -} - -public class AppUserProgressRepository : IAppUserProgressRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public AppUserProgressRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - public void Update(AppUserProgress userProgress) { - _context.Entry(userProgress).State = EntityState.Modified; + context.Entry(userProgress).State = EntityState.Modified; } public void Remove(AppUserProgress userProgress) { - _context.Remove(userProgress); + context.Remove(userProgress); } /// /// This will remove any entries that have chapterIds that no longer exists. This will execute the save as well. /// - public async Task CleanupAbandonedChapters() + /// + public async Task CleanupAbandonedChapters(CancellationToken ct = default) { - var chapterIds = _context.Chapter.Select(c => c.Id); + var chapterIds = context.Chapter.Select(c => c.Id); - var rowsToRemove = await _context.AppUserProgresses + var rowsToRemove = await context.AppUserProgresses .Where(progress => !chapterIds.Contains(progress.ChapterId)) - .ToListAsync(); + .ToListAsync(ct); - var rowsToRemoveBookmarks = await _context.AppUserBookmark + var rowsToRemoveBookmarks = await context.AppUserBookmark .Where(progress => !chapterIds.Contains(progress.ChapterId)) - .ToListAsync(); + .ToListAsync(ct); - var rowsToRemoveReadingLists = await _context.ReadingListItem + var rowsToRemoveReadingLists = await context.ReadingListItem .Where(item => !chapterIds.Contains(item.ChapterId)) - .ToListAsync(); + .ToListAsync(ct); - _context.RemoveRange(rowsToRemove); - _context.RemoveRange(rowsToRemoveBookmarks); - _context.RemoveRange(rowsToRemoveReadingLists); - return await _context.SaveChangesAsync() > 0 ? rowsToRemove.Count : 0; + context.RemoveRange(rowsToRemove); + context.RemoveRange(rowsToRemoveBookmarks); + context.RemoveRange(rowsToRemoveReadingLists); + return await context.SaveChangesAsync(ct) > 0 ? rowsToRemove.Count : 0; } /// - /// Checks if user has any progress against a library of passed type + /// Checks if a user has any progress against a library of a passed type /// /// /// + /// /// - public async Task UserHasProgress(LibraryType libraryType, int userId) + public async Task UserHasProgress(LibraryType libraryType, int userId, CancellationToken ct = default) { - var seriesIds = await _context.AppUserProgresses + var seriesIds = await context.AppUserProgresses .Where(aup => aup.PagesRead > 0 && aup.AppUserId == userId) .AsNoTracking() .Select(aup => aup.SeriesId) - .ToListAsync(); + .ToListAsync(ct); if (seriesIds.Count == 0) return false; - return await _context.Series + return await context.Series .Include(s => s.Library) .Where(s => seriesIds.Contains(s.Id) && s.Library.Type == libraryType) .AsNoTracking() - .AnyAsync(); + .AnyAsync(ct); } - public async Task HasAnyProgressOnSeriesAsync(int seriesId, int userId) + public async Task HasAnyProgressOnSeriesAsync(int seriesId, int userId, CancellationToken ct = default) { - return await _context.AppUserProgresses - .AnyAsync(aup => aup.PagesRead > 0 && aup.AppUserId == userId && aup.SeriesId == seriesId); + return await context.AppUserProgresses + .AnyAsync(aup => aup.PagesRead > 0 && aup.AppUserId == userId && aup.SeriesId == seriesId, ct); } /// @@ -119,118 +90,121 @@ public class AppUserProgressRepository : IAppUserProgressRepository /// /// /// + /// /// - public async Task> GetUserProgressForSeriesAsync(int seriesId, int userId) + public async Task> GetUserProgressForSeriesAsync(int seriesId, int userId, + CancellationToken ct = default) { - return await _context.AppUserProgresses + return await context.AppUserProgresses .Where(p => p.SeriesId == seriesId && p.AppUserId == userId && p.PagesRead > 0) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetAllProgress() + public async Task> GetAllProgress(CancellationToken ct = default) { - return await _context.AppUserProgresses.ToListAsync(); + return await context.AppUserProgresses.ToListAsync(ct); } /// /// Returns the latest progress in UTC /// + /// /// - public async Task GetLatestProgress() + public async Task GetLatestProgress(CancellationToken ct = default) { - return await _context.AppUserProgresses + return await context.AppUserProgresses .Select(d => d.LastModifiedUtc) .OrderByDescending(d => d) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task GetUserProgressDtoAsync(int chapterId, int userId) + public async Task GetUserProgressDtoAsync(int chapterId, int userId, CancellationToken ct = default) { - return await _context.AppUserProgresses + return await context.AppUserProgresses .Where(p => p.AppUserId == userId && p.ChapterId == chapterId) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(ct); } - public async Task AnyUserProgressForSeriesAsync(int seriesId, int userId) + public async Task AnyUserProgressForSeriesAsync(int seriesId, int userId, CancellationToken ct = default) { - return await _context.AppUserProgresses + return await context.AppUserProgresses .Where(p => p.SeriesId == seriesId && p.AppUserId == userId && p.PagesRead > 0) - .AnyAsync(); + .AnyAsync(ct); } - public async Task GetHighestFullyReadChapterForSeries(int seriesId, int userId) + public async Task GetHighestFullyReadChapterForSeries(int seriesId, int userId, CancellationToken ct = default) { - var list = await _context.AppUserProgresses - .Join(_context.Chapter, appUserProgresses => appUserProgresses.ChapterId, chapter => chapter.Id, + 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) - .Where(p => p.chapter.MaxNumber != Parser.SpecialVolumeNumber) + .Where(p => p.chapter.MaxNumber != ParserConstants.SpecialVolumeNumber) .Select(p => p.chapter.MaxNumber) - .ToListAsync(); + .ToListAsync(ct); return list.Count == 0 ? 0 : (int) list.DefaultIfEmpty().Max(d => d); } - public async Task GetHighestFullyReadVolumeForSeries(int seriesId, int userId) + public async Task GetHighestFullyReadVolumeForSeries(int seriesId, int userId, CancellationToken ct = default) { - var list = await _context.AppUserProgresses - .Join(_context.Chapter, appUserProgresses => appUserProgresses.ChapterId, chapter => chapter.Id, + 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) - .Where(p => p.chapter.MaxNumber != Parser.SpecialVolumeNumber) + .Where(p => p.chapter.MaxNumber != ParserConstants.SpecialVolumeNumber) .Select(p => p.chapter.Volume.MaxNumber) - .ToListAsync(); + .ToListAsync(ct); return list.Count == 0 ? 0 : list.DefaultIfEmpty().Max(); } - public async Task GetLatestProgressForSeries(int seriesId, int userId) + public async Task GetLatestProgressForSeries(int seriesId, int userId, CancellationToken ct = default) { - var list = await _context.AppUserProgresses.Where(p => p.AppUserId == userId && p.SeriesId == seriesId) + var list = await context.AppUserProgresses.Where(p => p.AppUserId == userId && p.SeriesId == seriesId) .Select(p => p.LastModifiedUtc) - .ToListAsync(); + .ToListAsync(ct); return list.Count == 0 ? null : list.DefaultIfEmpty().Max(); } - public async Task GetLatestProgressForVolume(int volumeId, int userId) + public async Task GetLatestProgressForVolume(int volumeId, int userId, CancellationToken ct = default) { - var list = await _context.AppUserProgresses.Where(p => p.AppUserId == userId && p.VolumeId == volumeId) + var list = await context.AppUserProgresses.Where(p => p.AppUserId == userId && p.VolumeId == volumeId) .Select(p => p.LastModifiedUtc) - .ToListAsync(); + .ToListAsync(ct); return list.Count == 0 ? null : list.DefaultIfEmpty().Max(); } - public async Task GetLatestProgressForChapter(int chapterId, int userId) + public async Task GetLatestProgressForChapter(int chapterId, int userId, CancellationToken ct = default) { - return await _context.AppUserProgresses + return await context.AppUserProgresses .Where(p => p.AppUserId == userId && p.ChapterId == chapterId) .Select(p => p.LastModifiedUtc) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task GetFirstProgressForSeries(int seriesId, int userId) + public async Task GetFirstProgressForSeries(int seriesId, int userId, CancellationToken ct = default) { - var list = await _context.AppUserProgresses.Where(p => p.AppUserId == userId && p.SeriesId == seriesId) + var list = await context.AppUserProgresses.Where(p => p.AppUserId == userId && p.SeriesId == seriesId) .Select(p => p.LastModifiedUtc) - .ToListAsync(); + .ToListAsync(ct); return list.Count == 0 ? null : list.DefaultIfEmpty().Min(); } - public async Task GetFirstProgressForUser(int userId) + public async Task GetFirstProgressForUser(int userId, CancellationToken ct = default) { - return await _context.AppUserProgresses + return await context.AppUserProgresses .Where(p => p.AppUserId == userId) .OrderBy(p => p.CreatedUtc) .Select(p => p.CreatedUtc) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task UpdateAllProgressThatAreMoreThanChapterPages() + public async Task UpdateAllProgressThatAreMoreThanChapterPages(CancellationToken ct = default) { - var updates = _context.AppUserProgresses - .Join(_context.Chapter, + var updates = context.AppUserProgresses + .Join(context.Chapter, progress => progress.ChapterId, chapter => chapter.Id, (progress, chapter) => new @@ -255,7 +229,7 @@ public class AppUserProgressRepository : IAppUserProgressRepository // Execute the batch SQL var batchSql = sqlBuilder.ToString(); - await _context.Database.ExecuteSqlRawAsync(batchSql); + await context.Database.ExecuteSqlRawAsync(batchSql, ct); } /// @@ -263,10 +237,12 @@ public class AppUserProgressRepository : IAppUserProgressRepository /// /// /// If 0, will pull all records + /// /// - public async Task> GetUserProgressForChapter(int chapterId, int userId = 0) + public async Task> GetUserProgressForChapter(int chapterId, int userId = 0, + CancellationToken ct = default) { - return await _context.AppUserProgresses + return await context.AppUserProgresses .WhereIf(userId > 0, p => p.AppUserId == userId) .Where(p => p.ChapterId == chapterId) .Include(p => p.AppUser) @@ -282,14 +258,13 @@ public class AppUserProgressRepository : IAppUserProgressRepository LastModifiedUtc = p.LastModifiedUtc, UserName = p.AppUser.UserName }) - .ToListAsync(); + .ToListAsync(ct); } -#nullable enable - public async Task GetUserProgressAsync(int chapterId, int userId) + public async Task GetUserProgressAsync(int chapterId, int userId, CancellationToken ct = default) { - return await _context.AppUserProgresses + return await context.AppUserProgresses .Where(p => p.ChapterId == chapterId && p.AppUserId == userId) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } } diff --git a/API/Data/Repositories/AppUserReadingProfileRepository.cs b/Kavita.Database/Repositories/AppUserReadingProfileRepository.cs similarity index 56% rename from API/Data/Repositories/AppUserReadingProfileRepository.cs rename to Kavita.Database/Repositories/AppUserReadingProfileRepository.cs index 541f17561..47ccfffec 100644 --- a/API/Data/Repositories/AppUserReadingProfileRepository.cs +++ b/Kavita.Database/Repositories/AppUserReadingProfileRepository.cs @@ -1,81 +1,25 @@ -#nullable enable using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.DTOs; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Extensions.QueryExtensions; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Common.Extensions; +using Kavita.Database.Extensions; +using Kavita.Models.DTOs; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; +namespace Kavita.Database.Repositories; -public interface IAppUserReadingProfileRepository -{ - - /// - /// Returns the reading profile to use for the given series - /// - /// - /// - /// - /// - /// - /// - Task GetProfileForSeries(int userId, int libraryId, int seriesId, int? activeDeviceId = null, bool skipImplicit = false); - - /// - /// Get all profiles assigned to a library - /// - /// - /// - /// - Task> GetProfilesForLibrary(int userId, int libraryId); - /// - /// Return the profile if it belongs the user - /// - /// - /// - /// - Task GetUserProfile(int userId, int profileId); - - /// - /// Returns all reading profiles for the user - /// - /// - /// - /// - Task> GetProfilesForUser(int userId, bool skipImplicit = false); - - /// - /// Returns all reading profiles for the user - /// - /// - /// - /// - Task> GetProfilesDtoForUser(int userId, bool skipImplicit = false); - /// - /// Is there a user reading profile with this name (normalized) - /// - /// - /// - /// - Task IsProfileNameInUse(int userId, string name); - - void Add(AppUserReadingProfile readingProfile); - void Update(AppUserReadingProfile readingProfile); - void Remove(AppUserReadingProfile readingProfile); - void RemoveRange(IEnumerable readingProfiles); -} - public class AppUserReadingProfileRepository(DataContext context, IMapper mapper): IAppUserReadingProfileRepository { - public Task GetProfileForSeries(int userId, int libraryId, int seriesId, int? activeDeviceId = null, bool skipImplicit = false) + public Task GetProfileForSeries(int userId, int libraryId, int seriesId, + int? activeDeviceId = null, bool skipImplicit = false, CancellationToken ct = default) { return context.AppUserReadingProfiles .Where(rp => rp.AppUserId == userId) @@ -88,52 +32,57 @@ public class AppUserReadingProfileRepository(DataContext context, IMapper mapper .ThenByDescending(rp => rp.LibraryIds.Contains(libraryId) && (rp.DeviceIds.Count == 0 || (activeDeviceId != null && rp.DeviceIds.Contains(activeDeviceId.Value)))) .ThenByDescending(rp => rp.LibraryIds.Contains(libraryId)) .ThenByDescending(rp => rp.Kind == ReadingProfileKind.Default) - .FirstAsync(); + .FirstAsync(ct); } - public Task> GetProfilesForLibrary(int userId, int libraryId) + public Task> GetProfilesForLibrary(int userId, int libraryId, + CancellationToken ct = default) { return context.AppUserReadingProfiles .Where(rp => rp.AppUserId == userId && rp.LibraryIds.Contains(libraryId)) - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetUserProfile(int userId, int profileId) + public async Task GetUserProfile(int userId, int profileId, CancellationToken ct = default) { return await context.AppUserReadingProfiles .Where(rp => rp.AppUserId == userId && rp.Id == profileId) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task> GetProfilesForUser(int userId, bool skipImplicit = false) + public async Task> GetProfilesForUser(int userId, bool skipImplicit = false, + CancellationToken ct = default) { return await context.AppUserReadingProfiles .Where(rp => rp.AppUserId == userId) .WhereIf(skipImplicit, rp => rp.Kind != ReadingProfileKind.Implicit) - .ToListAsync(); + .ToListAsync(ct); } /// /// Returns all Reading Profiles for the User /// /// + /// + /// /// - public async Task> GetProfilesDtoForUser(int userId, bool skipImplicit = false) + public async Task> GetProfilesDtoForUser(int userId, bool skipImplicit = false, + CancellationToken ct = default) { return await context.AppUserReadingProfiles .Where(rp => rp.AppUserId == userId) .WhereIf(skipImplicit, rp => rp.Kind != ReadingProfileKind.Implicit) .ProjectTo(mapper.ConfigurationProvider) - .ToListAsync(); + .ToListAsync(ct); } - public async Task IsProfileNameInUse(int userId, string name) + public async Task IsProfileNameInUse(int userId, string name, CancellationToken ct = default) { var normalizedName = name.ToNormalized(); return await context.AppUserReadingProfiles .Where(rp => rp.NormalizedName == normalizedName && rp.AppUserId == userId) - .AnyAsync(); + .AnyAsync(ct); } public void Add(AppUserReadingProfile readingProfile) diff --git a/Kavita.Database/Repositories/AppUserSmartFilterRepository.cs b/Kavita.Database/Repositories/AppUserSmartFilterRepository.cs new file mode 100644 index 000000000..8dbb904e7 --- /dev/null +++ b/Kavita.Database/Repositories/AppUserSmartFilterRepository.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Common.Helpers; +using Kavita.Database.Extensions; +using Kavita.Models.DTOs.Dashboard; +using Kavita.Models.Entities.User; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Repositories; + +public class AppUserSmartFilterRepository(DataContext context, IMapper mapper) : IAppUserSmartFilterRepository +{ + public void Update(AppUserSmartFilter filter) + { + context.Entry(filter).State = EntityState.Modified; + } + + public void Attach(AppUserSmartFilter filter) + { + context.AppUserSmartFilter.Attach(filter); + } + + public void Delete(AppUserSmartFilter filter) + { + context.AppUserSmartFilter.Remove(filter); + } + + public async Task> GetAllDtosByUserId(int userId, CancellationToken ct = default) + { + return await context.AppUserSmartFilter + .Where(f => f.AppUserId == userId) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); + } + + public Task> GetPagedDtosByUserIdAsync(int userId, UserParams userParams, + CancellationToken ct = default) + { + var filters = context.AppUserSmartFilter + .Where(f => f.AppUserId == userId) + .ProjectTo(mapper.ConfigurationProvider); + + return PagedList.CreateAsync(filters, userParams, ct); + } + + public async Task GetById(int smartFilterId, CancellationToken ct = default) + { + return await context.AppUserSmartFilter + .FirstOrDefaultAsync(d => d.Id == smartFilterId, ct); + } +} diff --git a/API/Data/Repositories/ChapterRepository.cs b/Kavita.Database/Repositories/ChapterRepository.cs similarity index 55% rename from API/Data/Repositories/ChapterRepository.cs rename to Kavita.Database/Repositories/ChapterRepository.cs index 568463f26..ddec4c795 100644 --- a/API/Data/Repositories/ChapterRepository.cs +++ b/Kavita.Database/Repositories/ChapterRepository.cs @@ -1,115 +1,61 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.DTOs; -using API.DTOs.Metadata; -using API.DTOs.Reader; -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.Tasks.Scanner.Parser; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Database.Extensions; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Extensions; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; -#nullable enable +namespace Kavita.Database.Repositories; -[Flags] -public enum ChapterIncludes + +public class ChapterRepository(DataContext context, IMapper mapper) : IChapterRepository { - None = 1, - Volumes = 2, - Files = 4, - People = 8, - Genres = 16, - Tags = 32, - ExternalReviews = 1 << 6, - ExternalRatings = 1 << 7 -} - -public interface IChapterRepository -{ - void Update(Chapter chapter); - void Remove(Chapter chapter); - void Remove(IList chapters); - Task> GetChaptersByIdsAsync(IList chapterIds, ChapterIncludes includes = ChapterIncludes.None); - Task GetChapterInfoDtoAsync(int chapterId); - Task GetChapterTotalPagesAsync(int chapterId); - Task GetChapterAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files); - Task GetChapterDtoAsync(int chapterId, int userId); - Task> GetChapterDtoByIdsAsync(IEnumerable chapterIds, int userId); - Task GetChapterMetadataDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files); - Task> GetFilesForChapterAsync(int chapterId); - Task GetFilesizeForChapterAsync(int chapterId); - Task> GetFilesizeForChaptersAsync(IList chapterIds); - Task> GetChaptersAsync(int volumeId, ChapterIncludes includes = ChapterIncludes.None); - Task> GetChapterDtosAsync(int volumeId, int userId); - Task> GetFilesForChaptersAsync(IReadOnlyList chapterIds); - Task GetChapterCoverImageAsync(int chapterId); - Task> GetAllCoverImagesAsync(); - Task> GetAllChaptersWithCoversInDifferentEncoding(EncodeFormat format); - Task> GetCoverImagesForLockedChaptersAsync(); - IQueryable GetChaptersForSeries(int seriesId); - Task> GetAllChaptersForSeries(int seriesId); - Task GetAverageUserRating(int chapterId, int userId); - Task> GetExternalChapterReviewDtos(int chapterId); - Task> GetExternalChapterReview(int chapterId); - Task> GetExternalChapterRatingDtos(int chapterId); - Task> GetExternalChapterRatings(int chapterId); - Task GetCurrentlyReadingChapterAsync(int seriesId, int userId); - Task GetFirstChapterForSeriesAsync(int seriesId, int userId); - Task GetFirstChapterForVolumeAsync(int volumeId, int userId); - Task> GetChapterDtosAsync(IEnumerable chapterIds, int userId); -} -public class ChapterRepository : IChapterRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public ChapterRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - public void Update(Chapter chapter) { - _context.Entry(chapter).State = EntityState.Modified; + context.Entry(chapter).State = EntityState.Modified; } public void Remove(Chapter chapter) { - _context.Chapter.Remove(chapter); + context.Chapter.Remove(chapter); } public void Remove(IList chapters) { - _context.Chapter.RemoveRange(chapters); + context.Chapter.RemoveRange(chapters); } - public async Task> GetChaptersByIdsAsync(IList chapterIds, ChapterIncludes includes = ChapterIncludes.None) + public async Task> GetChaptersByIdsAsync(IList chapterIds, + ChapterIncludes includes = ChapterIncludes.None, CancellationToken ct = default) { - return await _context.Chapter + return await context.Chapter .Where(c => chapterIds.Contains(c.Id)) .Includes(includes) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } /// /// Populates a partial IChapterInfoDto /// /// - public async Task GetChapterInfoDtoAsync(int chapterId) + public async Task GetChapterInfoDtoAsync(int chapterId, CancellationToken ct = default) { - var chapterInfo = await _context.Chapter + var chapterInfo = await context.Chapter .Where(c => c.Id == chapterId) - .Join(_context.Volume, c => c.VolumeId, v => v.Id, (chapter, volume) => new + .Join(context.Volume, c => c.VolumeId, v => v.Id, (chapter, volume) => new { ChapterNumber = chapter.MinNumber, VolumeNumber = volume.Name, @@ -119,7 +65,7 @@ public class ChapterRepository : IChapterRepository volume.SeriesId, chapter.Pages, }) - .Join(_context.Series, data => data.SeriesId, series => series.Id, (data, series) => new + .Join(context.Series, data => data.SeriesId, series => series.Id, (data, series) => new { data.ChapterNumber, data.VolumeNumber, @@ -149,49 +95,51 @@ public class ChapterRepository : IChapterRepository }) .AsNoTracking() .AsSplitQuery() - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); return chapterInfo; } - public Task GetChapterTotalPagesAsync(int chapterId) + public Task GetChapterTotalPagesAsync(int chapterId, CancellationToken ct = default) { - return _context.Chapter + return context.Chapter .Where(c => c.Id == chapterId) .Select(c => c.Pages) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task GetChapterDtoAsync(int chapterId, int userId) + public async Task GetChapterDtoAsync(int chapterId, int userId, CancellationToken ct = default) { - var chapter = await _context.Chapter + var chapter = await context.Chapter .Includes(ChapterIncludes.Files | ChapterIncludes.People) - .ProjectToWithProgress(_mapper, userId) + .ProjectToWithProgress(mapper, userId) .AsSplitQuery() - .FirstOrDefaultAsync(c => c.Id == chapterId); + .FirstOrDefaultAsync(c => c.Id == chapterId, ct); return chapter; } - public async Task> GetChapterDtoByIdsAsync(IEnumerable chapterIds, int userId) + public async Task> GetChapterDtoByIdsAsync(IEnumerable chapterIds, int userId, + CancellationToken ct = default) { - var chapters = await _context.Chapter + var chapters = await context.Chapter .Where(c => chapterIds.Contains(c.Id)) .Includes(ChapterIncludes.Files | ChapterIncludes.People) - .ProjectToWithProgress(_mapper, userId) + .ProjectToWithProgress(mapper, userId) .AsSplitQuery() - .ToListAsync() ; + .ToListAsync(ct) ; return chapters; } - public async Task GetChapterMetadataDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files) + public async Task GetChapterMetadataDtoAsync(int chapterId, + ChapterIncludes includes = ChapterIncludes.Files, CancellationToken ct = default) { - var chapter = await _context.Chapter + var chapter = await context.Chapter .Includes(includes) - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .AsNoTracking() .AsSplitQuery() - .SingleOrDefaultAsync(c => c.Id == chapterId); + .SingleOrDefaultAsync(c => c.Id == chapterId, ct); return chapter; } @@ -200,209 +148,226 @@ public class ChapterRepository : IChapterRepository /// Returns non-tracked files for a given chapterId /// /// + /// /// - public async Task> GetFilesForChapterAsync(int chapterId) + public async Task> GetFilesForChapterAsync(int chapterId, CancellationToken ct = default) { - return await _context.MangaFile + return await context.MangaFile .Where(c => chapterId == c.ChapterId) .AsNoTracking() - .ToListAsync(); - } - - public async Task GetFilesizeForChapterAsync(int chapterId) - { - return await _context.MangaFile - .Where(c => c.ChapterId == chapterId) - .SumAsync(c => c.Bytes); - } - - public async Task> GetFilesizeForChaptersAsync(IList chapterIds) - { - return await chapterIds.BatchToDictionaryAsync(50, batch => - _context.MangaFile - .Where(f => batch.Contains(f.ChapterId)) - .ToDictionaryAsync(f => f.ChapterId, f => f.Bytes)); + .ToListAsync(ct); } /// - /// Returns a Chapter for an Id. Includes linked s. + /// Returns a Chapter for an id. Includes linked s. /// /// /// + /// /// - public async Task GetChapterAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files) + public async Task GetChapterAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files, + CancellationToken ct = default) { - return await _context.Chapter + return await context.Chapter .Includes(includes) .OrderBy(c => c.SortOrder) - .FirstOrDefaultAsync(c => c.Id == chapterId); + .FirstOrDefaultAsync(c => c.Id == chapterId, ct); } /// /// Returns Chapters for a volume id. /// /// + /// + /// /// - public async Task> GetChaptersAsync(int volumeId, ChapterIncludes includes = ChapterIncludes.None) + public async Task> GetChaptersAsync(int volumeId, ChapterIncludes includes = ChapterIncludes.None, + CancellationToken ct = default) { - return await _context.Chapter + return await context.Chapter .Where(c => c.VolumeId == volumeId) .Includes(includes) .OrderBy(c => c.SortOrder) - .ToListAsync(); + .ToListAsync(ct); } /// /// Returns Chapters for a volume id with Progress /// /// + /// + /// /// - public async Task> GetChapterDtosAsync(int volumeId, int userId) + public async Task> GetChapterDtosAsync(int volumeId, int userId, CancellationToken ct = default) { - return await _context.Chapter + return await context.Chapter .Where(c => c.VolumeId == volumeId) .Includes(ChapterIncludes.Files | ChapterIncludes.People) .OrderBy(c => c.SortOrder) - .ProjectToWithProgress(_mapper, userId) - .ToListAsync(); + .ProjectToWithProgress(mapper, userId) + .ToListAsync(ct); } + /// /// Returns the cover image for a chapter id. /// /// + /// /// - public async Task GetChapterCoverImageAsync(int chapterId) + public async Task GetChapterCoverImageAsync(int chapterId, CancellationToken ct = default) { - return await _context.Chapter + return await context.Chapter .Where(c => c.Id == chapterId) .Select(c => c.CoverImage) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } - public async Task> GetAllCoverImagesAsync() + public async Task> GetAllCoverImagesAsync(CancellationToken ct = default) { - return (await _context.Chapter + return (await context.Chapter .Select(c => c.CoverImage) .Where(t => !string.IsNullOrEmpty(t)) - .ToListAsync())!; + .ToListAsync(ct))!; } - public async Task> GetAllChaptersWithCoversInDifferentEncoding(EncodeFormat format) + public async Task> GetAllChaptersWithCoversInDifferentEncoding(EncodeFormat format, + CancellationToken ct = default) { var extension = format.GetExtension(); - return await _context.Chapter + return await context.Chapter .Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension)) - .ToListAsync(); + .ToListAsync(ct); } /// /// Returns cover images for locked chapters /// /// - public async Task> GetCoverImagesForLockedChaptersAsync() + public async Task> GetCoverImagesForLockedChaptersAsync(CancellationToken ct = default) { - return (await _context.Chapter + return (await context.Chapter .Where(c => c.CoverImageLocked) .Select(c => c.CoverImage) .Where(t => !string.IsNullOrEmpty(t)) - .ToListAsync())!; + .ToListAsync(ct))!; } /// /// Returns non-tracked files for a set of /// /// List of chapter Ids + /// /// - public async Task> GetFilesForChaptersAsync(IReadOnlyList chapterIds) + public async Task> GetFilesForChaptersAsync(IReadOnlyList chapterIds, + CancellationToken ct = default) { - return await _context.MangaFile + return await context.MangaFile .Where(c => chapterIds.Contains(c.ChapterId)) .AsNoTracking() - .ToListAsync(); + .ToListAsync(ct); + } + + public async Task GetFilesizeForChapterAsync(int chapterId, CancellationToken ct = default) + { + return await context.MangaFile + .Where(c => c.ChapterId == chapterId) + .SumAsync(c => c.Bytes, cancellationToken: ct); + } + + public async Task> GetFilesizeForChaptersAsync(IList chapterIds, CancellationToken ct = default) + { + return await chapterIds.BatchToDictionaryAsync(50, batch => + context.MangaFile + .Where(f => batch.Contains(f.ChapterId)) + .ToDictionaryAsync(f => f.ChapterId, f => f.Bytes, cancellationToken: ct)); } /// /// Includes Volumes /// /// + /// /// - public IQueryable GetChaptersForSeries(int seriesId) + public IQueryable GetChaptersForSeries(int seriesId, CancellationToken ct = default) { - return _context.Chapter + return context.Chapter .Where(c => c.Volume.SeriesId == seriesId) .OrderBy(c => c.SortOrder) .Include(c => c.Volume); } - public async Task> GetAllChaptersForSeries(int seriesId) + public async Task> GetAllChaptersForSeries(int seriesId, CancellationToken ct = default) { - return await _context.Chapter + return await context.Chapter .Where(c => c.Volume.SeriesId == seriesId) .OrderBy(c => c.SortOrder) .Include(c => c.Volume) .Include(c => c.People) .ThenInclude(cp => cp.Person) - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetAverageUserRating(int chapterId, int userId) + public async Task GetAverageUserRating(int chapterId, int userId, CancellationToken ct = default) { - // If there is 0 or 1 rating and that rating is you, return 0 back - var countOfRatingsThatAreUser = await _context.AppUserChapterRating + // If there is a 0 or 1 rating and that rating is you, return 0 back + var countOfRatingsThatAreUser = await context.AppUserChapterRating .Where(r => r.ChapterId == chapterId && r.HasBeenRated) - .CountAsync(u => u.AppUserId == userId); + .CountAsync(u => u.AppUserId == userId, ct); + if (countOfRatingsThatAreUser == 1) { return 0; } - var avg = (await _context.AppUserChapterRating + + var avg = await context.AppUserChapterRating .Where(r => r.ChapterId == chapterId && r.HasBeenRated) - .AverageAsync(r => (int?) r.Rating)); + .AverageAsync(r => (int?) r.Rating, ct); + return avg.HasValue ? (int) (avg.Value * 20) : 0; } - public async Task> GetExternalChapterReviewDtos(int chapterId) + public async Task> GetExternalChapterReviewDtos(int chapterId, CancellationToken ct = default) { - return await _context.Chapter + return await context.Chapter .Where(c => c.Id == chapterId) .SelectMany(c => c.ExternalReviews) // Don't use ProjectTo, it fails to map int to float (??) - .Select(r => _mapper.Map(r)) - .ToListAsync(); + .Select(r => mapper.Map(r)) + .ToListAsync(ct); } - public async Task> GetExternalChapterReview(int chapterId) + public async Task> GetExternalChapterReview(int chapterId, CancellationToken ct = default) { - return await _context.Chapter + return await context.Chapter .Where(c => c.Id == chapterId) .SelectMany(c => c.ExternalReviews) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetExternalChapterRatingDtos(int chapterId) + public async Task> GetExternalChapterRatingDtos(int chapterId, CancellationToken ct = default) { - return await _context.Chapter + return await context.Chapter .Where(c => c.Id == chapterId) .SelectMany(c => c.ExternalRatings) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task> GetExternalChapterRatings(int chapterId) + public async Task> GetExternalChapterRatings(int chapterId, CancellationToken ct = default) { - return await _context.Chapter + return await context.Chapter .Where(c => c.Id == chapterId) .SelectMany(c => c.ExternalRatings) - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetCurrentlyReadingChapterAsync(int seriesId, int userId) + public async Task GetCurrentlyReadingChapterAsync(int seriesId, int userId, CancellationToken ct = default) { - var chapterWithProgress = await _context.AppUserProgresses + var chapterWithProgress = await context.AppUserProgresses .Where(p => p.AppUserId == userId) .Join( - _context.Chapter + context.Chapter .Include(c => c.Volume) .Include(c => c.Files), p => p.ChapterId, @@ -410,56 +375,65 @@ public class ChapterRepository : IChapterRepository (p, c) => new { Chapter = c, p.PagesRead } ) .Where(x => x.Chapter.Volume.SeriesId == seriesId) - .Where(x => x.Chapter.Volume.Number != Parser.LooseLeafVolumeNumber) + .Where(x => x.Chapter.Volume.Number != ParserConstants.LooseLeafVolumeNumber) .Where(x => x.PagesRead > 0 && x.PagesRead < x.Chapter.Pages) .OrderBy(x => x.Chapter.Volume.Number) .ThenBy(x => x.Chapter.SortOrder) .AsNoTracking() - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); if (chapterWithProgress == null) return null; // Map chapter to DTO - var dto = _mapper.Map(chapterWithProgress.Chapter); + var dto = mapper.Map(chapterWithProgress.Chapter); dto.PagesRead = chapterWithProgress.PagesRead; return dto; } - public async Task GetFirstChapterForSeriesAsync(int seriesId, int userId) + public async Task GetFirstChapterForSeriesAsync(int seriesId, int userId, CancellationToken ct = default) { // Get the chapter entity with proper ordering - return await _context.Chapter + return await context.Chapter .Include(c => c.Volume) .Include(c => c.Files) .Where(c => c.Volume.SeriesId == seriesId) .ApplyDefaultChapterOrdering() .AsNoTracking() - .ProjectToWithProgress(_mapper, userId) - .FirstOrDefaultAsync(); + .ProjectToWithProgress(mapper, userId) + .FirstOrDefaultAsync(ct); } - public async Task GetFirstChapterForVolumeAsync(int volumeId, int userId) + public async Task GetFirstChapterForVolumeAsync(int volumeId, int userId, CancellationToken ct = default) { // Get the chapter entity with proper ordering - return await _context.Chapter + return await context.Chapter .Include(c => c.Volume) .Include(c => c.Files) .Where(c => c.Volume.Id == volumeId) .ApplyDefaultChapterOrdering() .AsNoTracking() - .ProjectToWithProgress(_mapper, userId) - .FirstOrDefaultAsync(); + .ProjectToWithProgress(mapper, userId) + .FirstOrDefaultAsync(ct); } - public async Task> GetChapterDtosAsync(IEnumerable chapterIds, int userId) + public async Task> GetChapterDtosAsync(IEnumerable chapterIds, int userId, + CancellationToken ct = default) { var chapterIdList = chapterIds.ToList(); if (chapterIdList.Count == 0) return []; - return await _context.Chapter + return await context.Chapter .Where(c => chapterIdList.Contains(c.Id)) - .ProjectToWithProgress(_mapper, userId) - .ToListAsync(); + .ProjectToWithProgress(mapper, userId) + .ToListAsync(ct); + } + + public async Task GetSeriesIdForChapter(int chapterId, CancellationToken ct = default) + { + return await context.Chapter + .Where(chp => chp.Id == chapterId) + .Select(chp => chp.Volume.SeriesId) + .FirstOrDefaultAsync(ct); } } diff --git a/Kavita.Database/Repositories/ClientDeviceRepository.cs b/Kavita.Database/Repositories/ClientDeviceRepository.cs new file mode 100644 index 000000000..3d85e84a1 --- /dev/null +++ b/Kavita.Database/Repositories/ClientDeviceRepository.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Database.Extensions; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities.User; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Repositories; + +public class ClientDeviceRepository(DataContext context, IMapper mapper): IClientDeviceRepository +{ + public Task GetClientDeviceById(int id, int userId, CancellationToken cancellationToken = default) + { + return context.ClientDevice.FirstOrDefaultAsync(c => c.Id == id && c.AppUserId == userId, cancellationToken); + } + + public async Task GetClientDeviceByClientFingerprint(int userId, string uiFingerprint, CancellationToken cancellationToken) + { + return await context.ClientDevice + .Include(d => d.History.OrderByDescending(h => h.CapturedAtUtc).Take(1)) + .FirstOrDefaultAsync(d => + d.AppUserId == userId && + d.UiFingerprint == uiFingerprint && + d.IsActive, cancellationToken: cancellationToken); + } + + public async Task> GetUserDevicesAsync(int userId, bool includeInactive = false, CancellationToken cancellationToken = default) + { + return await context.ClientDevice + .Where(d => d.AppUserId == userId) + .WhereIf(!includeInactive, d => d.IsActive) + .OrderByDescending(d => d.LastSeenUtc) + .ToListAsync(cancellationToken: cancellationToken); + } + + public async Task> GetUserDeviceDtosAsync(int userId, bool includeInactive = false, CancellationToken cancellationToken = default) + { + return await context.ClientDevice + .Where(d => d.AppUserId == userId) + .WhereIf(!includeInactive, d => d.IsActive) + .OrderByDescending(d => d.LastSeenUtc) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(cancellationToken: cancellationToken); + } + + public async Task> GetAllUserDeviceDtos(bool includeInactive = false, + CancellationToken cancellationToken = default) + { + return await context.ClientDevice + .WhereIf(!includeInactive, d => d.IsActive) + .OrderByDescending(d => d.LastSeenUtc) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(cancellationToken: cancellationToken); + } +} diff --git a/Kavita.Database/Repositories/CollectionTagRepository.cs b/Kavita.Database/Repositories/CollectionTagRepository.cs new file mode 100644 index 000000000..20158b23e --- /dev/null +++ b/Kavita.Database/Repositories/CollectionTagRepository.cs @@ -0,0 +1,239 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Database.Extensions; +using Kavita.Models.DTOs.Collection; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Models.Extensions; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Repositories; + + + +public class CollectionTagRepository(DataContext context, IMapper mapper) : ICollectionTagRepository +{ + public void Remove(AppUserCollection tag) + { + context.AppUserCollection.Remove(tag); + } + + public void Update(AppUserCollection tag) + { + context.Entry(tag).State = EntityState.Modified; + } + + /// + /// Removes any collection tags without any series + /// + /// + public async Task RemoveCollectionsWithoutSeries(CancellationToken ct = default) + { + var tagsToDelete = await context.AppUserCollection + .Include(c => c.Items) + .Where(c => c.Items.Count == 0) + .AsSplitQuery() + .ToListAsync(ct); + + context.RemoveRange(tagsToDelete); + + return await context.SaveChangesAsync(ct); + } + + public async Task> GetAllCollectionsAsync( + CollectionIncludes includes = CollectionIncludes.None, CancellationToken ct = default) + { + return await context.AppUserCollection + .OrderBy(c => c.NormalizedTitle) + .Includes(includes) + .ToListAsync(ct); + } + + public async Task> GetCollectionDtosAsync(int userId, + bool includePromoted = false, CancellationToken ct = default) + { + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); + return await context.AppUserCollection + .Where(uc => uc.AppUserId == userId || (includePromoted && uc.Promoted)) + .WhereIf(ageRating.AgeRating != AgeRating.NotApplicable, uc => uc.AgeRating <= ageRating.AgeRating) + .OrderBy(uc => uc.Title) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); + } + + public async Task GetCollectionDtoAsync(int collectionId, int userId, CancellationToken ct = default) + { + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); + return await context.AppUserCollection + .Where(uc => (uc.AppUserId == userId || uc.Promoted) && uc.Id == collectionId) + .WhereIf(ageRating.AgeRating != AgeRating.NotApplicable, uc => uc.AgeRating <= ageRating.AgeRating) + .OrderBy(uc => uc.Title) + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(ct); + } + + public async Task> GetCollectionDtosPagedAsync(int userId, UserParams userParams, + bool includePromoted = false, CancellationToken ct = default) + { + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); + var collections = context.AppUserCollection + .Where(uc => uc.AppUserId == userId || (includePromoted && uc.Promoted)) + .WhereIf(ageRating.AgeRating != AgeRating.NotApplicable, uc => uc.AgeRating <= ageRating.AgeRating) + .OrderBy(uc => uc.Title) + .ProjectTo(mapper.ConfigurationProvider); + + return await PagedList.CreateAsync(collections, userParams, ct); + } + + public async Task> GetCollectionDtosBySeriesAsync(int userId, int seriesId, + bool includePromoted = false, CancellationToken ct = default) + { + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); + return await context.AppUserCollection + .Where(uc => uc.AppUserId == userId || (includePromoted && uc.Promoted)) + .Where(uc => uc.Items.Any(s => s.Id == seriesId)) + .WhereIf(ageRating.AgeRating != AgeRating.NotApplicable, uc => uc.AgeRating <= ageRating.AgeRating) + .OrderBy(uc => uc.Title) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); + } + + public async Task GetCoverImageAsync(int collectionTagId, CancellationToken ct = default) + { + return await context.AppUserCollection + .Where(c => c.Id == collectionTagId) + .Select(c => c.CoverImage) + .SingleOrDefaultAsync(ct); + } + + public async Task> GetAllCoverImagesAsync(CancellationToken ct = default) + { + return await context.AppUserCollection + .Select(t => t.CoverImage) + .Where(t => !string.IsNullOrEmpty(t)) + .ToListAsync(ct); + } + + /// + /// If any tag exists for that given user's collections + /// + /// + /// + /// + /// + public async Task CollectionExists(string title, int userId, CancellationToken ct = default) + { + var normalized = title.ToNormalized(); + return await context.AppUserCollection + .Where(uc => uc.AppUserId == userId) + .AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized), ct); + } + + public async Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, + CancellationToken ct = default) + { + var extension = encodeFormat.GetExtension(); + return await context.AppUserCollection + .Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension)) + .ToListAsync(ct); + } + + public async Task> GetRandomCoverImagesAsync(int collectionId, CancellationToken ct = default) + { + var random = new Random(); + var data = await context.AppUserCollection + .Where(t => t.Id == collectionId) + .SelectMany(uc => uc.Items.Select(series => series.CoverImage)) + .Where(t => !string.IsNullOrEmpty(t)) + .ToListAsync(ct); + + return data + .OrderBy(_ => random.Next()) + .Take(4) + .ToList(); + } + + public async Task> GetCollectionsForUserAsync(int userId, + CollectionIncludes includes = CollectionIncludes.None, CancellationToken ct = default) + { + return await context.AppUserCollection + .Where(c => c.AppUserId == userId) + .Includes(includes) + .ToListAsync(ct); + } + + public async Task UpdateCollectionAgeRating(AppUserCollection tag, CancellationToken ct = default) + { + var maxAgeRating = await context.AppUserCollection + .Where(t => t.Id == tag.Id) + .SelectMany(uc => uc.Items.Select(s => s.Metadata)) + .Select(sm => sm.AgeRating) + .ToListAsync(ct); + + + tag.AgeRating = maxAgeRating.Count != 0 ? maxAgeRating.Max() : AgeRating.Unknown; + await context.SaveChangesAsync(ct); + } + + public async Task> GetCollectionsByIds(IEnumerable tags, + CollectionIncludes includes = CollectionIncludes.None, CancellationToken ct = default) + { + return await context.AppUserCollection + .Where(c => tags.Contains(c.Id)) + .Includes(includes) + .AsSplitQuery() + .ToListAsync(ct); + } + + public async Task> GetAllCollectionsForSyncing(DateTime expirationTime, + CancellationToken ct = default) + { + return await context.AppUserCollection + .Where(c => c.Source == ScrobbleProvider.Mal) + .Where(c => c.LastSyncUtc <= expirationTime) + .Include(c => c.Items) + .AsSplitQuery() + .ToListAsync(ct); + } + + public async Task GetCollectionAsync(int tagId, + CollectionIncludes includes = CollectionIncludes.None, CancellationToken ct = default) + { + return await context.AppUserCollection + .Where(c => c.Id == tagId) + .Includes(includes) + .AsSplitQuery() + .SingleOrDefaultAsync(ct); + } + + private async Task GetUserAgeRestriction(int userId, CancellationToken ct = default) + { + return await context.AppUser + .AsNoTracking() + .Where(u => u.Id == userId) + .Select(u => + new AgeRestriction(){ + AgeRating = u.AgeRestriction, + IncludeUnknowns = u.AgeRestrictionIncludeUnknowns + }) + .SingleAsync(ct); + } + + public async Task> SearchTagDtosAsync(string searchQuery, int userId, CancellationToken ct = default) + { + var userRating = await GetUserAgeRestriction(userId, ct); + return await context.AppUserCollection + .Search(searchQuery, userId, userRating) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); + } +} diff --git a/API/Data/Repositories/CoverDbRepository.cs b/Kavita.Database/Repositories/CoverDbRepository.cs similarity index 95% rename from API/Data/Repositories/CoverDbRepository.cs rename to Kavita.Database/Repositories/CoverDbRepository.cs index 5d7b4b726..8dd7049c2 100644 --- a/API/Data/Repositories/CoverDbRepository.cs +++ b/Kavita.Database/Repositories/CoverDbRepository.cs @@ -2,12 +2,12 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using API.DTOs.CoverDb; -using API.Entities.Person; +using Kavita.Models.DTOs.CoverDb; +using Kavita.Models.Entities.Person; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; -namespace API.Data.Repositories; +namespace Kavita.Database.Repositories; #nullable enable /// diff --git a/Kavita.Database/Repositories/DeviceRepository.cs b/Kavita.Database/Repositories/DeviceRepository.cs new file mode 100644 index 000000000..688be844e --- /dev/null +++ b/Kavita.Database/Repositories/DeviceRepository.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Models.DTOs.Device.EmailDevice; +using Kavita.Models.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Repositories; + +public class DeviceRepository(DataContext context, IMapper mapper) : IDeviceRepository +{ + public void Update(Device device) + { + context.Entry(device).State = EntityState.Modified; + } + + public async Task> GetDevicesForUserAsync(int userId, CancellationToken ct = default) + { + return await context.Device + .Where(d => d.AppUserId == userId) + .OrderBy(d => d.LastUsed) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); + } + + public async Task GetDeviceById(int deviceId, CancellationToken ct = default) + { + return await context.Device + .Where(d => d.Id == deviceId) + .SingleOrDefaultAsync(ct); + } +} diff --git a/Kavita.Database/Repositories/EmailHistoryRepository.cs b/Kavita.Database/Repositories/EmailHistoryRepository.cs new file mode 100644 index 000000000..e912107d0 --- /dev/null +++ b/Kavita.Database/Repositories/EmailHistoryRepository.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Email; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Repositories; + +public class EmailHistoryRepository(DataContext context, IMapper mapper) : IEmailHistoryRepository +{ + public async Task> GetEmailDtos(UserParams userParams, CancellationToken ct = default) + { + return await context.EmailHistory + .OrderByDescending(h => h.SendDate) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); + } +} diff --git a/Kavita.Database/Repositories/EpubFontRepository.cs b/Kavita.Database/Repositories/EpubFontRepository.cs new file mode 100644 index 000000000..59120112e --- /dev/null +++ b/Kavita.Database/Repositories/EpubFontRepository.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.Common.Extensions; +using Kavita.Models; +using Kavita.Models.DTOs.Font; +using Kavita.Models.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Repositories; + +public class EpubFontRepository(DataContext context, IMapper mapper) : IEpubFontRepository +{ + public void Add(EpubFont font) + { + context.Add(font); + } + + public void Remove(EpubFont font) + { + context.Remove(font); + } + + public void Update(EpubFont font) + { + context.Entry(font).State = EntityState.Modified; + } + + public async Task> GetFontDtosAsync(CancellationToken ct = default) + { + return await context.EpubFont + .OrderBy(s => s.Name == Defaults.DefaultFont ? -1 : 0) + .ThenBy(s => s) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); + } + + public async Task GetFontDtoAsync(int fontId, CancellationToken ct = default) + { + return await context.EpubFont + .Where(f => f.Id == fontId) + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(ct); + } + + public async Task GetFontDtoByNameAsync(string name, CancellationToken ct = default) + { + return await context.EpubFont + .Where(f => f.NormalizedName.Equals(name.ToNormalized())) + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(ct); + } + + public async Task> GetFontsAsync(CancellationToken ct = default) + { + return await context.EpubFont.ToListAsync(ct); + } + + public async Task GetFontAsync(int fontId, CancellationToken ct = default) + { + return await context.EpubFont + .Where(f => f.Id == fontId) + .FirstOrDefaultAsync(ct); + } + + public async Task IsFontInUseAsync(int fontId, CancellationToken ct = default) + { + return await context.AppUserReadingProfiles + .Join(context.EpubFont, + preference => preference.BookReaderFontFamily, + font => font.Name, + (preference, font) => new { preference, font }) + .AnyAsync(joined => joined.font.Id == fontId, ct); + } + +} diff --git a/API/Data/Repositories/ExternalSeriesMetadataRepository.cs b/Kavita.Database/Repositories/ExternalSeriesMetadataRepository.cs similarity index 59% rename from API/Data/Repositories/ExternalSeriesMetadataRepository.cs rename to Kavita.Database/Repositories/ExternalSeriesMetadataRepository.cs index 1e7302b05..fc31955b1 100644 --- a/API/Data/Repositories/ExternalSeriesMetadataRepository.cs +++ b/Kavita.Database/Repositories/ExternalSeriesMetadataRepository.cs @@ -1,127 +1,101 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.DTOs; -using API.DTOs.KavitaPlus.Manage; -using API.DTOs.Recommendation; -using API.DTOs.SeriesDetail; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Extensions.QueryExtensions; -using API.Helpers; -using API.Services.Plus; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.Common.Helpers; +using Kavita.Database.Extensions; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.KavitaPlus.Manage; +using Kavita.Models.DTOs.Recommendation; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; -#nullable enable +namespace Kavita.Database.Repositories; -public interface IExternalSeriesMetadataRepository +public class ExternalSeriesMetadataRepository(DataContext context, IMapper mapper) : 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 NeedsDataRefresh(int seriesId); - Task GetSeriesDetailPlusDto(int seriesId); - Task LinkRecommendationsToSeries(Series series); - Task IsBlacklistedSeries(int seriesId); - Task> GetSeriesThatNeedExternalMetadata(int limit, bool includeStaleData = false); - Task> GetAllSeries(ManageMatchFilterDto filter, UserParams userParams); -} - -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); + context.ExternalSeriesMetadata.Attach(metadata); } public void Attach(ExternalRating rating) { - _context.ExternalRating.Attach(rating); + context.ExternalRating.Attach(rating); } public void Attach(ExternalReview review) { - _context.ExternalReview.Attach(review); + context.ExternalReview.Attach(review); } public void Remove(IEnumerable? reviews) { if (reviews == null) return; - _context.ExternalReview.RemoveRange(reviews); + context.ExternalReview.RemoveRange(reviews); } public void Remove(IEnumerable? ratings) { if (ratings == null) return; - _context.ExternalRating.RemoveRange(ratings); + context.ExternalRating.RemoveRange(ratings); } public void Remove(IEnumerable? recommendations) { if (recommendations == null) return; - _context.ExternalRecommendation.RemoveRange(recommendations); + context.ExternalRecommendation.RemoveRange(recommendations); } public void Remove(ExternalSeriesMetadata? metadata) { if (metadata == null) return; - _context.ExternalSeriesMetadata.Remove(metadata); + context.ExternalSeriesMetadata.Remove(metadata); } /// /// Returns the ExternalSeriesMetadata entity for the given Series including all linked tables /// /// + /// /// - public Task GetExternalSeriesMetadata(int seriesId) + public Task GetExternalSeriesMetadata(int seriesId, CancellationToken ct = default) { - return _context.ExternalSeriesMetadata + 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(); + .FirstOrDefaultAsync(ct); } - public async Task NeedsDataRefresh(int seriesId) + public async Task NeedsDataRefresh(int seriesId, CancellationToken ct = default) { - // TODO: Add unit test - return await _context.ExternalSeriesMetadata + return await context.ExternalSeriesMetadata .Where(s => s.SeriesId == seriesId) .Select(s => s.ValidUntilUtc) .Where(date => date < DateTime.UtcNow) - .AnyAsync(); + .AnyAsync(ct); } - public async Task GetSeriesDetailPlusDto(int seriesId) + public async Task GetSeriesDetailPlusDto(int seriesId, CancellationToken ct = default) { - // TODO: Add unit test - var seriesDetailDto = await _context.ExternalSeriesMetadata + var seriesDetailDto = await context.ExternalSeriesMetadata .Where(m => m.SeriesId == seriesId) .Include(m => m.ExternalRatings) .Include(m => m.ExternalReviews) .Include(m => m.ExternalRecommendations) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); if (seriesDetailDto == null) { @@ -130,7 +104,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor var externalSeriesRecommendations = seriesDetailDto.ExternalRecommendations .Where(r => r.SeriesId == null) - .Select(r => _mapper.Map(r)) + .Select(mapper.Map) .ToList(); var ownedIds = seriesDetailDto.ExternalRecommendations @@ -138,11 +112,11 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor .Select(r => r.SeriesId) .ToList(); - var ownedSeriesRecommendations = await _context.Series + var ownedSeriesRecommendations = await context.Series .Where(s => ownedIds.Contains(s.Id)) .OrderBy(s => s.SortName.ToLower()) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); IEnumerable reviews = []; if (seriesDetailDto.ExternalReviews != null && seriesDetailDto.ExternalReviews.Any()) @@ -150,7 +124,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor reviews = seriesDetailDto.ExternalReviews .Select(r => { - var ret = _mapper.Map(r); + var ret = mapper.Map(r); ret.IsExternal = true; return ret; }) @@ -161,7 +135,7 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor if (seriesDetailDto.ExternalRatings != null && seriesDetailDto.ExternalRatings.Count != 0) { ratings = seriesDetailDto.ExternalRatings - .Select(r => _mapper.Map(r)); + .Select(mapper.Map); } @@ -183,36 +157,38 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor /// Searches Recommendations without a SeriesId on record and attempts to link based on Series Name/Localized Name /// /// + /// /// - public async Task LinkRecommendationsToSeries(Series series) + public async Task LinkRecommendationsToSeries(Series series, CancellationToken ct = default) { - var recMatches = await _context.ExternalRecommendation + 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(); + .ToListAsync(ct); foreach (var rec in recMatches) { rec.SeriesId = series.Id; } - await _context.SaveChangesAsync(); + await context.SaveChangesAsync(ct); } - public Task IsBlacklistedSeries(int seriesId) + public Task IsBlacklistedSeries(int seriesId, CancellationToken ct = default) { - return _context.Series + return context.Series .Where(s => s.Id == seriesId) .Select(s => s.IsBlacklisted) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task> GetSeriesThatNeedExternalMetadata(int limit, bool includeStaleData = false) + public async Task> GetSeriesThatNeedExternalMetadata(int limit, bool includeStaleData = false, + CancellationToken ct = default) { - return await _context.Series - .Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type)) + return await context.Series + .Where(s => !IExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type)) .Where(s => s.Library.AllowMetadataMatching) .WhereIf(includeStaleData, s => s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.ValidUntilUtc < DateTime.UtcNow) .Where(s => s.ExternalSeriesMetadata == null || s.ExternalSeriesMetadata.AniListId == 0) @@ -221,21 +197,22 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor .ThenBy(s => s.NormalizedName) .Select(s => s.Id) .Take(limit) - .ToListAsync(); + .ToListAsync(ct); } - public Task> GetAllSeries(ManageMatchFilterDto filter, UserParams userParams) + public Task> GetAllSeries(ManageMatchFilterDto filter, UserParams userParams, + CancellationToken ct = default) { - var source = _context.Series + var source = context.Series .Include(s => s.Library) .Include(s => s.ExternalSeriesMetadata) - .Where(s => !ExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type)) + .Where(s => !IExternalMetadataService.NonEligibleLibraryTypes.Contains(s.Library.Type)) .Where(s => s.Library.AllowMetadataMatching) .WhereIf(filter.LibraryType >= 0, s => s.Library.Type == (LibraryType) filter.LibraryType) .FilterMatchState(filter.MatchStateOption) .OrderBy(s => s.NormalizedName) - .ProjectTo(_mapper.ConfigurationProvider); + .ProjectTo(mapper.ConfigurationProvider); - return PagedList.CreateAsync(source, userParams); + return PagedList.CreateAsync(source, userParams, ct); } } diff --git a/API/Data/Repositories/GenreRepository.cs b/Kavita.Database/Repositories/GenreRepository.cs similarity index 55% rename from API/Data/Repositories/GenreRepository.cs rename to Kavita.Database/Repositories/GenreRepository.cs index 7f705e8ae..5fecc8c75 100644 --- a/API/Data/Repositories/GenreRepository.cs +++ b/Kavita.Database/Repositories/GenreRepository.cs @@ -1,115 +1,92 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.DTOs.Metadata; -using API.DTOs.Metadata.Browse; -using API.Entities; -using API.Extensions; -using API.Extensions.QueryExtensions; -using API.Helpers; -using API.Services.Tasks.Scanner.Parser; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Database.Extensions; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Metadata.Browse; +using Kavita.Models.Entities; +using Kavita.Models.Extensions; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; -#nullable enable +namespace Kavita.Database.Repositories; -public interface IGenreRepository +public class GenreRepository(DataContext context, IMapper mapper) : IGenreRepository { - void Attach(Genre genre); - void Remove(Genre genre); - Task FindByNameAsync(string genreName); - Task> GetAllGenresAsync(); - Task> GetAllGenresByNamesAsync(IEnumerable normalizedNames); - Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false); - Task> GetAllGenreDtosForLibrariesAsync(int userId, IList? libraryIds = null, QueryContext context = QueryContext.None); - Task GetCountAsync(); - Task GetRandomGenre(); - Task GetGenreById(int id); - Task> GetAllGenresNotInListAsync(ICollection genreNames); - Task> GetBrowseableGenre(int userId, UserParams userParams); -} - -public class GenreRepository : IGenreRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public GenreRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - public void Attach(Genre genre) { - _context.Genre.Attach(genre); + context.Genre.Attach(genre); } public void Remove(Genre genre) { - _context.Genre.Remove(genre); + context.Genre.Remove(genre); } - public async Task FindByNameAsync(string genreName) + public async Task FindByNameAsync(string genreName, CancellationToken ct = default) { var normalizedName = genreName.ToNormalized(); - return await _context.Genre - .FirstOrDefaultAsync(g => g.NormalizedTitle != null && g.NormalizedTitle.Equals(normalizedName)); + return await context.Genre + .FirstOrDefaultAsync(g => g.NormalizedTitle != null && g.NormalizedTitle.Equals(normalizedName), cancellationToken: ct); } - public async Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false) + public async Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false, CancellationToken ct = default) { - var genresWithNoConnections = await _context.Genre + var genresWithNoConnections = await context.Genre .Include(p => p.SeriesMetadatas) .Include(p => p.Chapters) .Where(p => p.SeriesMetadatas.Count == 0 && p.Chapters.Count == 0) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(cancellationToken: ct); - _context.Genre.RemoveRange(genresWithNoConnections); + context.Genre.RemoveRange(genresWithNoConnections); - await _context.SaveChangesAsync(); + await context.SaveChangesAsync(ct); } - public async Task GetCountAsync() + public async Task GetCountAsync(CancellationToken ct = default) { - return await _context.Genre.CountAsync(); + return await context.Genre.CountAsync(cancellationToken: ct); } - public async Task GetRandomGenre() + public async Task GetRandomGenre(CancellationToken ct = default) { - var genreCount = await GetCountAsync(); + var genreCount = await GetCountAsync(ct); if (genreCount == 0) return null; var randomIndex = new Random().Next(0, genreCount); - return await _context.Genre + return await context.Genre .Skip(randomIndex) .Take(1) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(cancellationToken: ct); } - public async Task GetGenreById(int id) + public async Task GetGenreById(int id, CancellationToken ct = default) { - return await _context.Genre + return await context.Genre .Where(g => g.Id == id) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(cancellationToken: ct); } - public async Task> GetAllGenresAsync() + public async Task> GetAllGenresAsync(CancellationToken ct = default) { - return await _context.Genre.ToListAsync(); + return await context.Genre.ToListAsync(ct); } - public async Task> GetAllGenresByNamesAsync(IEnumerable normalizedNames) + public async Task> GetAllGenresByNamesAsync(IEnumerable normalizedNames, + CancellationToken ct = default) { - return await _context.Genre + return await context.Genre .Where(g => normalizedNames.Contains(g.NormalizedTitle)) - .ToListAsync(); + .ToListAsync(ct); } /// @@ -118,26 +95,29 @@ public class GenreRepository : IGenreRepository /// /// /// + /// + /// /// - public async Task> GetAllGenreDtosForLibrariesAsync(int userId, IList? libraryIds = null, QueryContext context = QueryContext.None) + public async Task> GetAllGenreDtosForLibrariesAsync(int userId, IList? libraryIds = null, + QueryContext context1 = QueryContext.None, CancellationToken ct = default) { - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = await _context.Library.GetUserLibraries(userId, context).ToListAsync(); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); + var userLibs = await context.Library.GetUserLibraries(userId, context1).ToListAsync(ct); if (libraryIds is {Count: > 0}) { userLibs = userLibs.Where(libraryIds.Contains).ToList(); } - return await _context.Series + return await context.Series .Where(s => userLibs.Contains(s.LibraryId)) .RestrictAgainstAgeRestriction(userRating) .SelectMany(s => s.Metadata.Genres) .AsSplitQuery() .Distinct() .OrderBy(p => p.NormalizedTitle) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } /// @@ -145,22 +125,24 @@ public class GenreRepository : IGenreRepository /// Normalizes genres for lookup, but returns non-normalized names for creation. /// /// The list of genre names (non-normalized). + /// /// A list of genre names that do not exist in the system. - public async Task> GetAllGenresNotInListAsync(ICollection genreNames) + public async Task> GetAllGenresNotInListAsync(ICollection genreNames, + CancellationToken ct = default) { // Group the genres by their normalized names, keeping track of the original names var normalizedToOriginalMap = genreNames .Distinct() - .GroupBy(Parser.Normalize) + .GroupBy(g => g.ToNormalized()) .ToDictionary(group => group.Key, group => group.First()); // Take the first original name for each normalized name var normalizedGenreNames = normalizedToOriginalMap.Keys.ToList(); // Query the database for existing genres using the normalized names - var existingGenres = await _context.Genre + var existingGenres = await context.Genre .Where(g => normalizedGenreNames.Contains(g.NormalizedTitle)) // Assuming you have a normalized field .Select(g => g.NormalizedTitle) - .ToListAsync(); + .ToListAsync(ct); // Find the normalized genres that do not exist in the database var missingGenres = normalizedGenreNames.Except(existingGenres).ToList(); @@ -169,16 +151,19 @@ public class GenreRepository : IGenreRepository return missingGenres.Select(normalizedName => normalizedToOriginalMap[normalizedName]).ToList(); } - public async Task> GetBrowseableGenre(int userId, UserParams userParams) + public async Task> GetBrowseableGenre(int userId, UserParams userParams, + CancellationToken ct = default) { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); - var allLibrariesCount = await _context.Library.CountAsync(); - var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); + var allLibrariesCount = await context.Library.CountAsync(ct); + var userLibs = await context.Library.GetUserLibraries(userId).ToListAsync(ct); - var seriesIds = await _context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id).ToListAsync(); + var seriesIds = await context.Series + .Where(s => userLibs.Contains(s.LibraryId)) + .Select(s => s.Id).ToListAsync(ct); - var query = _context.Genre + var query = context.Genre .RestrictAgainstAgeRestriction(ageRating) .WhereIf(allLibrariesCount != userLibs.Count, genre => genre.Chapters.Any(cp => seriesIds.Contains(cp.Volume.SeriesId)) || @@ -200,6 +185,6 @@ public class GenreRepository : IGenreRepository }) .OrderBy(g => g.Title); - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize, ct); } } diff --git a/API/Data/Repositories/LibraryRepository.cs b/Kavita.Database/Repositories/LibraryRepository.cs similarity index 53% rename from API/Data/Repositories/LibraryRepository.cs rename to Kavita.Database/Repositories/LibraryRepository.cs index 9c32b169c..b001b1aa8 100644 --- a/API/Data/Repositories/LibraryRepository.cs +++ b/Kavita.Database/Repositories/LibraryRepository.cs @@ -2,179 +2,136 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.DTOs; -using API.DTOs.Filtering; -using API.DTOs.JumpBar; -using API.DTOs.Metadata; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Extensions.QueryExtensions; -using API.Services.Tasks.Scanner.Parser; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; using Kavita.Common.Extensions; +using Kavita.Database.Extensions; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.JumpBar; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Extensions; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; -#nullable enable +namespace Kavita.Database.Repositories; -[Flags] -public enum LibraryIncludes + + +public class LibraryRepository(DataContext context, IMapper mapper) : ILibraryRepository { - None = 1, - Series = 2, - AppUser = 4, - Folders = 8, - FileTypes = 16, - ExcludePatterns = 32 -} - -public interface ILibraryRepository -{ - void Add(Library library); - void Update(Library library); - void Delete(Library? library); - Task> GetLibraryDtosAsync(); - Task GetLibraryDtoByIdAsync(int libraryId); - Task GetLiteLibraryDtoByIdAsync(int libraryId); - Task LibraryExists(string libraryName); - Task GetLibraryForIdAsync(int libraryId, LibraryIncludes includes = LibraryIncludes.None); - Task> GetLibraryDtosForUsernameAsync(string userName); - Task> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None, bool track = true); - Task> GetLibrariesForUserIdAsync(int userId); - Task> 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); - Task> GetAllAgeRatingsDtosForLibrariesAsync(List libraryIds); - Task> GetAllLanguagesForLibrariesAsync(List? libraryIds); - IEnumerable GetAllPublicationStatusesDtosForLibrariesAsync(List libraryIds); - Task DoAnySeriesFoldersMatch(IEnumerable folders); - Task GetLibraryCoverImageAsync(int libraryId); - Task> GetAllCoverImagesAsync(); - Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat); - Task GetAllowsScrobblingBySeriesId(int seriesId); - - Task> GetLibraryTypesBySeriesIdsAsync(IList seriesIds); -} - -public class LibraryRepository : ILibraryRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public LibraryRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - public void Add(Library library) { - _context.Library.Add(library); + context.Library.Add(library); } public void Update(Library library) { - _context.Entry(library).State = EntityState.Modified; + context.Entry(library).State = EntityState.Modified; } public void Delete(Library? library) { if (library == null) return; - _context.Library.Remove(library); + context.Library.Remove(library); } - public async Task> GetLibraryDtosForUsernameAsync(string userName) + public async Task> GetLibraryDtosForUsernameAsync(string userName, CancellationToken ct = default) { - return await _context.Library + return await context.Library .Include(l => l.AppUsers) .Include(l => l.LibraryFileTypes) .Include(l => l.LibraryExcludePatterns) .Where(library => library.AppUsers.Any(x => x.UserName!.Equals(userName))) .OrderBy(l => l.Name) - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } /// /// Returns all libraries including their AppUsers + extra includes /// /// + /// + /// /// - public async Task> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None, bool track = true) + public async Task> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None, + bool track = true, CancellationToken ct = default) { - var query = _context.Library + var query = context.Library .Include(l => l.AppUsers) .Includes(includes) .AsSplitQuery(); - if (track) return await query.ToListAsync(); + if (track) return await query.ToListAsync(ct); - return await query.AsNoTracking().ToListAsync(); + return await query.AsNoTracking().ToListAsync(ct); } /// /// This does not track /// /// + /// /// - public async Task> GetLibrariesForUserIdAsync(int userId) + public async Task> GetLibrariesForUserIdAsync(int userId, CancellationToken ct = default) { - return await _context.Library + return await context.Library .Include(l => l.AppUsers) .Where(l => l.AppUsers.Select(ap => ap.Id).Contains(userId)) .AsNoTracking() - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetLibraryIdsForUserIdAsync(int userId, QueryContext queryContext = QueryContext.None) + public async Task> GetLibraryIdsForUserIdAsync(int userId, QueryContext queryContext = QueryContext.None, + CancellationToken ct = default) { - return await _context.Library + return await context.Library .IsRestricted(queryContext) .Where(l => l.AppUsers.Select(ap => ap.Id).Contains(userId)) .Select(l => l.Id) - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetLibraryTypeAsync(int libraryId) + public async Task GetLibraryTypeAsync(int libraryId, CancellationToken ct = default) { - return await _context.Library + return await context.Library .Where(l => l.Id == libraryId) .AsNoTracking() .Select(l => l.Type) - .FirstAsync(); + .FirstAsync(ct); } - public async Task GetLibraryTypeBySeriesIdAsync(int seriesId) + public async Task GetLibraryTypeBySeriesIdAsync(int seriesId, CancellationToken ct = default) { - return await _context.Series + return await context.Series .Where(s => s.Id == seriesId) .Select(s => s.Library.Type) - .FirstAsync(); + .FirstAsync(ct); } - public async Task> GetLibraryForIdsAsync(IEnumerable libraryIds, LibraryIncludes includes = LibraryIncludes.None) + public async Task> GetLibraryForIdsAsync(IEnumerable libraryIds, + LibraryIncludes includes = LibraryIncludes.None, CancellationToken ct = default) { - return await _context.Library + return await context.Library .Where(x => libraryIds.Contains(x.Id)) .Includes(includes) - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetTotalFiles() + public async Task GetTotalFiles(CancellationToken ct = default) { - return await _context.MangaFile.CountAsync(); + return await context.MangaFile.CountAsync(ct); } - public IEnumerable GetJumpBarAsync(int libraryId) + public IEnumerable GetJumpBarAsync(int libraryId, CancellationToken ct = default) { - var seriesSortCharacters = _context.Series.Where(s => s.LibraryId == libraryId) + var seriesSortCharacters = context.Series.Where(s => s.LibraryId == libraryId) .Select(s => s.SortName!.ToUpper()) .OrderBy(s => s) .AsEnumerable() @@ -203,69 +160,61 @@ public class LibraryRepository : ILibraryRepository /// /// Returns all Libraries with their Folders /// + /// /// - public async Task> GetLibraryDtosAsync() + public async Task> GetLibraryDtosAsync(CancellationToken ct = default) { - return await _context.Library + return await context.Library .Include(f => f.Folders) .Include(l => l.LibraryFileTypes) .OrderBy(l => l.Name) - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .AsSplitQuery() .AsNoTracking() - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetLibraryDtoByIdAsync(int libraryId) + public async Task GetLibraryDtoByIdAsync(int libraryId, CancellationToken ct = default) { - return await _context.Library + return await context.Library .Include(f => f.Folders) .Include(l => l.LibraryFileTypes) - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .AsSplitQuery() - .FirstOrDefaultAsync(l => l.Id == libraryId); + .FirstOrDefaultAsync(l => l.Id == libraryId, ct); } - public async Task GetLiteLibraryDtoByIdAsync(int libraryId) + public async Task GetLiteLibraryDtoByIdAsync(int libraryId, CancellationToken ct = default) { - return await _context.Library - .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(l => l.Id == libraryId); + return await context.Library + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(l => l.Id == libraryId, ct); } - public async Task GetLibraryForIdAsync(int libraryId, LibraryIncludes includes = LibraryIncludes.None) + public async Task GetLibraryForIdAsync(int libraryId, LibraryIncludes includes = LibraryIncludes.None, + CancellationToken ct = default) { - var query = _context.Library + var query = context.Library .Where(x => x.Id == libraryId) .Includes(includes); - return await query.SingleOrDefaultAsync(); + return await query.SingleOrDefaultAsync(ct); } - public async Task LibraryExists(string libraryName) + public async Task LibraryExists(string libraryName, CancellationToken ct = default) { - return await _context.Library + return await context.Library .AsNoTracking() - .AnyAsync(x => x.Name != null && x.Name.Equals(libraryName)); - } - - public async Task> GetLibrariesForUserAsync(AppUser user) - { - return await _context.Library - .Where(library => library.AppUsers.Contains(user)) - .Include(l => l.Folders) - .AsNoTracking() - .AsSplitQuery() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .AnyAsync(x => x.Name != null && x.Name.Equals(libraryName), ct); } - public async Task> GetAllAgeRatingsDtosForLibrariesAsync(List libraryIds) + public async Task> GetAllAgeRatingsDtosForLibrariesAsync(List libraryIds, + CancellationToken ct = default) { - return await _context.Series + return await context.Series .Where(s => libraryIds.Contains(s.LibraryId)) .Select(s => s.Metadata.AgeRating) .Distinct() @@ -274,22 +223,23 @@ public class LibraryRepository : ILibraryRepository Value = s, Title = s.ToDescription() }) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetAllLanguagesForLibrariesAsync(List? libraryIds) + public async Task> GetAllLanguagesForLibrariesAsync(List? libraryIds, + CancellationToken ct = default) { - var ret = await _context.Series + var ret = await context.Series .WhereIf(libraryIds is {Count: > 0} , s => libraryIds!.Contains(s.LibraryId)) .Select(s => s.Metadata.Language) .AsSplitQuery() .AsNoTracking() .Distinct() - .ToListAsync(); + .ToListAsync(ct); return ret .Where(s => !string.IsNullOrEmpty(s)) - .DistinctBy(Parser.Normalize) + .DistinctBy(l => l.ToNormalized()) .Select(GetCulture) .Where(s => s != null) .OrderBy(s => s.Title) @@ -318,9 +268,10 @@ public class LibraryRepository : ILibraryRepository }; } - public IEnumerable GetAllPublicationStatusesDtosForLibrariesAsync(List libraryIds) + public IEnumerable GetAllPublicationStatusesDtosForLibrariesAsync(List libraryIds, + CancellationToken ct = default) { - return _context.Series + return context.Series .Where(s => libraryIds.Contains(s.LibraryId)) .AsSplitQuery() .Select(s => s.Metadata.PublicationStatus) @@ -338,54 +289,57 @@ public class LibraryRepository : ILibraryRepository /// Checks if any series folders match the folders passed in /// /// + /// /// - public async Task DoAnySeriesFoldersMatch(IEnumerable folders) + public async Task DoAnySeriesFoldersMatch(IEnumerable folders, CancellationToken ct = default) { - var normalized = folders.Select(Parser.NormalizePath); - return await _context.Series.AnyAsync(s => normalized.Contains(s.FolderPath)); + var normalized = folders.Select(f => f.NormalizePath()); + return await context.Series.AnyAsync(s => normalized.Contains(s.FolderPath), ct); } - public Task GetLibraryCoverImageAsync(int libraryId) + public Task GetLibraryCoverImageAsync(int libraryId, CancellationToken ct = default) { - return _context.Library + return context.Library .Where(l => l.Id == libraryId) .Select(l => l.CoverImage) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } - public async Task> GetAllCoverImagesAsync() + public async Task> GetAllCoverImagesAsync(CancellationToken ct = default) { - return (await _context.ReadingList + return (await context.ReadingList .Select(t => t.CoverImage) .Where(t => !string.IsNullOrEmpty(t)) - .ToListAsync())!; + .ToListAsync(ct))!; } - public async Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat) + public async Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, + CancellationToken ct = default) { var extension = encodeFormat.GetExtension(); - return await _context.Library + return await context.Library .Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension)) - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetAllowsScrobblingBySeriesId(int seriesId) + public async Task GetAllowsScrobblingBySeriesId(int seriesId, CancellationToken ct = default) { - return await _context.Series.Where(s => s.Id == seriesId) + return await context.Series.Where(s => s.Id == seriesId) .Select(s => s.Library.AllowScrobbling) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } - public async Task> GetLibraryTypesBySeriesIdsAsync(IList seriesIds) + public async Task> GetLibraryTypesBySeriesIdsAsync(IList seriesIds, + CancellationToken ct = default) { - return await _context.Series + 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); + .ToDictionaryAsync(entity => entity.Id, entity => entity.Type, ct); } } diff --git a/Kavita.Database/Repositories/MangaFileRepository.cs b/Kavita.Database/Repositories/MangaFileRepository.cs new file mode 100644 index 000000000..bd414df5d --- /dev/null +++ b/Kavita.Database/Repositories/MangaFileRepository.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Kavita.API.Repositories; +using Kavita.Models.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Repositories; + +public class MangaFileRepository(DataContext context) : IMangaFileRepository +{ + public void Update(MangaFile file) + { + context.Entry(file).State = EntityState.Modified; + } + + public async Task> GetAllWithMissingExtension(CancellationToken ct = default) + { + return await context.MangaFile + .Where(f => string.IsNullOrEmpty(f.Extension)) + .ToListAsync(ct); + } + + public async Task GetByKoreaderHash(string hash, CancellationToken ct = default) + { + if (string.IsNullOrEmpty(hash)) return null; + + return await context.MangaFile + .FirstOrDefaultAsync(f => f.KoreaderHash != null && + f.KoreaderHash.Equals(hash.ToUpper()), ct); + } +} diff --git a/Kavita.Database/Repositories/MediaErrorRepository.cs b/Kavita.Database/Repositories/MediaErrorRepository.cs new file mode 100644 index 000000000..12db86b1f --- /dev/null +++ b/Kavita.Database/Repositories/MediaErrorRepository.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Models.DTOs.MediaErrors; +using Kavita.Models.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Repositories; + +public class MediaErrorRepository(DataContext context, IMapper mapper) : IMediaErrorRepository +{ + public void Attach(MediaError? error) + { + if (error == null) return; + context.MediaError.Attach(error); + } + + public void Remove(MediaError? error) + { + if (error == null) return; + context.MediaError.Remove(error); + } + + public void Remove(IList errors) + { + context.MediaError.RemoveRange(errors); + } + + public Task Find(string filename, CancellationToken ct = default) + { + return context.MediaError + .Where(e => e.FilePath == filename) + .FirstOrDefaultAsync(ct); + } + + public async Task> GetAllErrorDtosAsync(CancellationToken ct = default) + { + return await context.MediaError + .OrderByDescending(m => m.Created) + .ProjectTo(mapper.ConfigurationProvider) + .AsNoTracking() + .ToListAsync(ct); + } + + public Task ExistsAsync(MediaError error, CancellationToken ct = default) + { + return context.MediaError.AnyAsync(m => m.FilePath.Equals(error.FilePath) + && m.Comment.Equals(error.Comment) + && m.Details.Equals(error.Details), ct + ); + } + + public async Task DeleteAll(CancellationToken ct = default) + { + await context.MediaError.ExecuteDeleteAsync(ct); + } + + public Task> GetAllErrorsAsync(IList comments, CancellationToken ct = default) + { + return context.MediaError + .Where(m => comments.Contains(m.Comment)) + .ToListAsync(ct); + } +} diff --git a/API/Data/Repositories/PersonRepository.cs b/Kavita.Database/Repositories/PersonRepository.cs similarity index 52% rename from API/Data/Repositories/PersonRepository.cs rename to Kavita.Database/Repositories/PersonRepository.cs index 3ad2f5dee..f0aac5da5 100644 --- a/API/Data/Repositories/PersonRepository.cs +++ b/Kavita.Database/Repositories/PersonRepository.cs @@ -1,152 +1,88 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.Data.Misc; -using API.DTOs; -using API.DTOs.Filtering.v2; -using API.DTOs.Metadata.Browse; -using API.DTOs.Metadata.Browse.Requests; -using API.DTOs.Person; -using API.Entities.Enums; -using API.Entities.Person; -using API.Extensions; -using API.Extensions.QueryExtensions; -using API.Extensions.QueryExtensions.Filtering; -using API.Helpers; -using API.Helpers.Converters; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Database.Converters; +using Kavita.Database.Extensions; +using Kavita.Database.Extensions.Filters; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.DTOs.Metadata.Browse; +using Kavita.Models.DTOs.Metadata.Browse.Requests; +using Kavita.Models.DTOs.Person; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Person; +using Kavita.Models.Extensions; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; -#nullable enable +namespace Kavita.Database.Repositories; -[Flags] -public enum PersonIncludes +public class PersonRepository(DataContext context, IMapper mapper) : IPersonRepository { - None = 1 << 0, - Aliases = 1 << 1, - ChapterPeople = 1 << 2, - SeriesPeople = 1 << 3, - - All = Aliases | ChapterPeople | SeriesPeople, -} - -public interface IPersonRepository -{ - void Attach(Person person); - void Attach(IEnumerable person); - void Remove(Person person); - void Remove(ChapterPeople person); - void Remove(SeriesMetadataPeople person); - void Update(Person person); - - Task> GetAllPeople(PersonIncludes includes = PersonIncludes.Aliases); - Task> GetAllPersonDtosAsync(int userId, PersonIncludes includes = PersonIncludes.None); - Task> GetAllPersonDtosByRoleAsync(int userId, PersonRole role, PersonIncludes includes = PersonIncludes.None); - Task RemoveAllPeopleNoLongerAssociated(); - Task> GetAllPeopleDtosForLibrariesAsync(int userId, List? libraryIds = null, PersonIncludes includes = PersonIncludes.None); - - Task GetCoverImageAsync(int personId); - Task> GetAllCoverImagesAsync(); - Task GetCoverImageByNameAsync(string name); - Task> GetRolesForPersonByName(int personId, int userId); - Task> GetBrowsePersonDtos(int userId, BrowsePersonFilterDto filter, UserParams userParams); - Task GetPersonById(int personId, PersonIncludes includes = PersonIncludes.None); - Task GetPersonDtoByName(string name, int userId, PersonIncludes includes = PersonIncludes.Aliases); - /// - /// Returns a person matched on normalized name or alias - /// - /// - /// - /// - Task GetPersonByNameOrAliasAsync(string name, PersonIncludes includes = PersonIncludes.Aliases); - Task IsNameUnique(string name); - - Task> GetSeriesKnownFor(int personId, int userId); - Task> GetChaptersForPersonByRole(int personId, int userId, PersonRole role); - /// - /// Returns all people with a matching name, or alias - /// - /// - /// - /// - Task> GetPeopleByNames(List normalizedNames, PersonIncludes includes = PersonIncludes.Aliases); - Task GetPersonByAniListId(int aniListId, PersonIncludes includes = PersonIncludes.Aliases); - - Task> SearchPeople(string searchQuery, PersonIncludes includes = PersonIncludes.Aliases); - - Task AnyAliasExist(string alias); -} - -public class PersonRepository : IPersonRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public PersonRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - public void Attach(Person person) { - _context.Person.Attach(person); + context.Person.Attach(person); } public void Attach(IEnumerable person) { - _context.Person.AttachRange(person); + context.Person.AttachRange(person); } public void Remove(Person person) { - _context.Person.Remove(person); + context.Person.Remove(person); } public void Remove(ChapterPeople person) { - _context.ChapterPeople.Remove(person); + context.ChapterPeople.Remove(person); } public void Remove(SeriesMetadataPeople person) { - _context.SeriesMetadataPeople.Remove(person); + context.SeriesMetadataPeople.Remove(person); } public void Update(Person person) { - _context.Person.Update(person); + context.Person.Update(person); } - public async Task RemoveAllPeopleNoLongerAssociated() + public async Task RemoveAllPeopleNoLongerAssociated(CancellationToken ct = default) { - var peopleWithNoConnections = await _context.Person + var peopleWithNoConnections = await context.Person .Include(p => p.SeriesMetadataPeople) .Include(p => p.ChapterPeople) .Where(p => p.SeriesMetadataPeople.Count == 0 && p.ChapterPeople.Count == 0) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); - _context.Person.RemoveRange(peopleWithNoConnections); + context.Person.RemoveRange(peopleWithNoConnections); - await _context.SaveChangesAsync(); + await context.SaveChangesAsync(ct); } - public async Task> GetAllPeopleDtosForLibrariesAsync(int userId, List? libraryIds = null, PersonIncludes includes = PersonIncludes.Aliases) + public async Task> GetAllPeopleDtosForLibrariesAsync(int userId, List? libraryIds = null, + PersonIncludes includes = PersonIncludes.None, CancellationToken ct = default) { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); + var userLibs = await context.Library.GetUserLibraries(userId).ToListAsync(ct); if (libraryIds is {Count: > 0}) { userLibs = userLibs.Where(libraryIds.Contains).ToList(); } - return await _context.Series + return await context.Series .Where(s => userLibs.Contains(s.LibraryId)) .RestrictAgainstAgeRestriction(ageRating) .SelectMany(s => s.Metadata.People.Select(p => p.Person)) @@ -155,81 +91,83 @@ public class PersonRepository : IPersonRepository .OrderBy(p => p.Name) .AsNoTracking() .AsSplitQuery() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task GetCoverImageAsync(int personId) + public async Task GetCoverImageAsync(int personId, CancellationToken ct = default) { - return await _context.Person + return await context.Person .Where(c => c.Id == personId) .Select(c => c.CoverImage) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } - public async Task> GetAllCoverImagesAsync() + public async Task> GetAllCoverImagesAsync(CancellationToken ct = default) { - return await _context.Person + return await context.Person .Select(p => p.CoverImage) - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetCoverImageByNameAsync(string name) + public async Task GetCoverImageByNameAsync(string name, CancellationToken ct = default) { var normalized = name.ToNormalized(); - return await _context.Person + return await context.Person .Where(c => c.NormalizedName == normalized) .Select(c => c.CoverImage) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } - public async Task> GetRolesForPersonByName(int personId, int userId) + public async Task> GetRolesForPersonByName(int personId, int userId, + CancellationToken ct = default) { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = _context.Library.GetUserLibraries(userId); + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); + var userLibs = context.Library.GetUserLibraries(userId); // Query roles from ChapterPeople - var chapterRoles = await _context.Person + var chapterRoles = await context.Person .Where(p => p.Id == personId) .SelectMany(p => p.ChapterPeople) .RestrictAgainstAgeRestriction(ageRating) .RestrictByLibrary(userLibs) .Select(cp => cp.Role) .Distinct() - .ToListAsync(); + .ToListAsync(ct); // Query roles from SeriesMetadataPeople - var seriesRoles = await _context.Person + var seriesRoles = await context.Person .Where(p => p.Id == personId) .SelectMany(p => p.SeriesMetadataPeople) .RestrictAgainstAgeRestriction(ageRating) .RestrictByLibrary(userLibs) .Select(smp => smp.Role) .Distinct() - .ToListAsync(); + .ToListAsync(ct); // Combine and return distinct roles return chapterRoles.Union(seriesRoles).Distinct(); } - public async Task> GetBrowsePersonDtos(int userId, BrowsePersonFilterDto filter, UserParams userParams) + public async Task> GetBrowsePersonDtos(int userId, BrowsePersonFilterDto filter, + UserParams userParams, CancellationToken ct = default) { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); - var query = await CreateFilteredPersonQueryable(userId, filter, ageRating); + var query = await CreateFilteredPersonQueryable(userId, filter, ageRating, ct); - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize, ct); } - private async Task> CreateFilteredPersonQueryable(int userId, BrowsePersonFilterDto filter, AgeRestriction ageRating) + private async Task> CreateFilteredPersonQueryable(int userId, BrowsePersonFilterDto filter, AgeRestriction ageRating, CancellationToken ct = default) { - var allLibrariesCount = await _context.Library.CountAsync(); - var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); + var allLibrariesCount = await context.Library.CountAsync(ct); + var userLibs = await context.Library.GetUserLibraries(userId).ToListAsync(ct); - var seriesIds = await _context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id).ToListAsync(); + var seriesIds = await context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id).ToListAsync(ct); - var query = _context.Person.AsNoTracking(); + var query = context.Person.AsNoTracking(); // Apply filtering based on statements query = BuildPersonFilterQuery(userId, filter, query); @@ -298,51 +236,54 @@ public class PersonRepository : IPersonRepository return limit <= 0 ? query : query.Take(limit); } - public async Task GetPersonById(int personId, PersonIncludes includes = PersonIncludes.None) + public async Task GetPersonById(int personId, PersonIncludes includes = PersonIncludes.None, + CancellationToken ct = default) { - return await _context.Person.Where(p => p.Id == personId) + return await context.Person.Where(p => p.Id == personId) .Includes(includes) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task GetPersonDtoByName(string name, int userId, PersonIncludes includes = PersonIncludes.Aliases) + public async Task GetPersonDtoByName(string name, int userId, + PersonIncludes includes = PersonIncludes.Aliases, CancellationToken ct = default) { var normalized = name.ToNormalized(); - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = _context.Library.GetUserLibraries(userId); + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); + var userLibs = context.Library.GetUserLibraries(userId); - return await _context.Person + return await context.Person .Where(p => p.NormalizedName == normalized) .Includes(includes) .RestrictAgainstAgeRestriction(ageRating) .RestrictByLibrary(userLibs) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(ct); } - public Task GetPersonByNameOrAliasAsync(string name, PersonIncludes includes = PersonIncludes.Aliases) + public Task GetPersonByNameOrAliasAsync(string name, PersonIncludes includes = PersonIncludes.Aliases, + CancellationToken ct = default) { var normalized = name.ToNormalized(); - return _context.Person + return context.Person .Includes(includes) .Where(p => p.NormalizedName == normalized || p.Aliases.Any(pa => pa.NormalizedAlias == normalized)) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task IsNameUnique(string name) + public async Task IsNameUnique(string name, CancellationToken ct = default) { // Should this use Normalized to check? - return !(await _context.Person + return !await context.Person .Includes(PersonIncludes.Aliases) - .AnyAsync(p => p.Name == name || p.Aliases.Any(pa => pa.Alias == name))); + .AnyAsync(p => p.Name == name || p.Aliases.Any(pa => pa.Alias == name), ct); } - public async Task> GetSeriesKnownFor(int personId, int userId) + public async Task> GetSeriesKnownFor(int personId, int userId, CancellationToken ct = default) { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); + var userLibs = await context.Library.GetUserLibraries(userId).ToListAsync(ct); - return await _context.Person + return await context.Person .Where(p => p.Id == personId) .SelectMany(p => p.SeriesMetadataPeople) .Select(smp => smp.SeriesMetadata) @@ -352,16 +293,17 @@ public class PersonRepository : IPersonRepository .Distinct() .OrderByDescending(s => s.ExternalSeriesMetadata.AverageExternalRating) .Take(20) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectToWithProgress(mapper.ConfigurationProvider, userId) + .ToListAsync(ct); } - public async Task> GetChaptersForPersonByRole(int personId, int userId, PersonRole role) + public async Task> GetChaptersForPersonByRole(int personId, int userId, + PersonRole role, CancellationToken ct = default) { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = _context.Library.GetUserLibraries(userId); + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); + var userLibs = context.Library.GetUserLibraries(userId); - return await _context.ChapterPeople + return await context.ChapterPeople .Where(cp => cp.PersonId == personId && cp.Role == role) .Select(cp => cp.Chapter) .RestrictAgainstAgeRestriction(ageRating) @@ -369,81 +311,87 @@ public class PersonRepository : IPersonRepository .OrderBy(ch => ch.Volume.MinNumber) // Group/Sort volumes as well .ThenBy(ch => ch.SortOrder) .Take(20) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectToWithProgress(mapper.ConfigurationProvider, userId) + .ToListAsync(ct); } - public async Task> GetPeopleByNames(List normalizedNames, PersonIncludes includes = PersonIncludes.Aliases) + public async Task> GetPeopleByNames(List normalizedNames, + PersonIncludes includes = PersonIncludes.Aliases, CancellationToken ct = default) { - return await _context.Person + return await context.Person .Includes(includes) .Where(p => normalizedNames.Contains(p.NormalizedName) || p.Aliases.Any(pa => normalizedNames.Contains(pa.NormalizedAlias))) .OrderBy(p => p.Name) - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetPersonByAniListId(int aniListId, PersonIncludes includes = PersonIncludes.Aliases) + public async Task GetPersonByAniListId(int aniListId, PersonIncludes includes = PersonIncludes.Aliases, + CancellationToken ct = default) { - return await _context.Person + return await context.Person .Where(p => p.AniListId == aniListId) .Includes(includes) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task> SearchPeople(string searchQuery, PersonIncludes includes = PersonIncludes.Aliases) + public async Task> SearchPeople(string searchQuery, + PersonIncludes includes = PersonIncludes.Aliases, CancellationToken ct = default) { searchQuery = searchQuery.ToNormalized(); - return await _context.Person + return await context.Person .Includes(includes) .Where(p => EF.Functions.Like(p.NormalizedName, $"%{searchQuery}%") || p.Aliases.Any(pa => EF.Functions.Like(pa.NormalizedAlias, $"%{searchQuery}%"))) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task AnyAliasExist(string alias) + public async Task AnyAliasExist(string alias, CancellationToken ct = default) { var normalizedAlias = alias.ToNormalized(); - return await _context.PersonAlias.AnyAsync(pa => pa.NormalizedAlias == normalizedAlias); + return await context.PersonAlias.AnyAsync(pa => pa.NormalizedAlias == normalizedAlias, ct); } - public async Task> GetAllPeople(PersonIncludes includes = PersonIncludes.Aliases) + public async Task> GetAllPeople(PersonIncludes includes = PersonIncludes.Aliases, + CancellationToken ct = default) { - return await _context.Person + return await context.Person .Includes(includes) .OrderBy(p => p.Name) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetAllPersonDtosAsync(int userId, PersonIncludes includes = PersonIncludes.None) + public async Task> GetAllPersonDtosAsync(int userId, PersonIncludes includes = PersonIncludes.None, + CancellationToken ct = default) { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = _context.Library.GetUserLibraries(userId); + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); + var userLibs = context.Library.GetUserLibraries(userId); - return await _context.Person + return await context.Person .Includes(includes) .RestrictAgainstAgeRestriction(ageRating) .RestrictByLibrary(userLibs) .OrderBy(p => p.Name) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task> GetAllPersonDtosByRoleAsync(int userId, PersonRole role, PersonIncludes includes = PersonIncludes.None) + public async Task> GetAllPersonDtosByRoleAsync(int userId, PersonRole role, + PersonIncludes includes = PersonIncludes.None, CancellationToken ct = default) { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = _context.Library.GetUserLibraries(userId); + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); + var userLibs = context.Library.GetUserLibraries(userId); - return await _context.Person + return await context.Person .Where(p => p.SeriesMetadataPeople.Any(smp => smp.Role == role) || p.ChapterPeople.Any(cp => cp.Role == role)) // Filter by role in both series and chapters .Includes(includes) .RestrictAgainstAgeRestriction(ageRating) .RestrictByLibrary(userLibs) .OrderBy(p => p.Name) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } } diff --git a/API/Data/Repositories/ReadingListRepository.cs b/Kavita.Database/Repositories/ReadingListRepository.cs similarity index 59% rename from API/Data/Repositories/ReadingListRepository.cs rename to Kavita.Database/Repositories/ReadingListRepository.cs index a78f3c2ea..9d15a3ef6 100644 --- a/API/Data/Repositories/ReadingListRepository.cs +++ b/Kavita.Database/Repositories/ReadingListRepository.cs @@ -1,115 +1,64 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.DTOs.Person; -using API.DTOs.ReadingLists; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Extensions.QueryExtensions; -using API.Helpers; -using API.Services; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Database.Extensions; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Extensions; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; -#nullable enable +namespace Kavita.Database.Repositories; -[Flags] -public enum ReadingListIncludes +public class ReadingListRepository(DataContext context, IMapper mapper) : IReadingListRepository { - None = 1, - Items = 2, - ItemChapter = 4, -} - -public interface IReadingListRepository -{ - Task> GetReadingListDtosForUserAsync(int userId, bool includePromoted, UserParams userParams, bool sortByLastModified = true); - Task GetReadingListByIdAsync(int readingListId, ReadingListIncludes includes = ReadingListIncludes.None); - Task> GetReadingListItemDtosByIdAsync(int readingListId, int userId, UserParams? userParams = null); - Task GetReadingListDtoByIdAsync(int readingListId, int userId); - Task GetReadingListDtoByTitleAsync(int userId, string title); - Task> GetReadingListItemsByIdAsync(int readingListId); - Task> GetReadingListDtosForSeriesAndUserAsync(int userId, int seriesId, - bool includePromoted); - Task> GetReadingListDtosForChapterAndUserAsync(int userId, int chapterId, - bool includePromoted); - void Remove(ReadingListItem item); - void Add(ReadingList list); - void BulkRemove(IEnumerable items); - void Update(ReadingList list); - Task Count(); - Task GetCoverImageAsync(int readingListId); - Task> GetRandomCoverImagesAsync(int readingListId); - Task> GetAllCoverImagesAsync(); - Task ReadingListExists(string name); - Task ReadingListExistsForUser(string name, int userId); - IEnumerable GetReadingListPeopleAsync(int readingListId, PersonRole role); - Task GetReadingListAllPeopleAsync(int readingListId); - Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat); - Task RemoveReadingListsWithoutSeries(); - Task GetReadingListByTitleAsync(string name, int userId, ReadingListIncludes includes = ReadingListIncludes.Items); - Task> GetReadingListsByIds(IList ids, ReadingListIncludes includes = ReadingListIncludes.Items); - Task> GetReadingListsBySeriesId(int seriesId, ReadingListIncludes includes = ReadingListIncludes.Items); - Task GetReadingListInfoAsync(int readingListId); - Task AnyUserReadingProgressAsync(int readingListId, int userId); - Task GetContinueReadingPoint(int readingListId, int userId); - Task GetReadingListItemCountAsync(int readingListId, int userId); -} - -public class ReadingListRepository : IReadingListRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public ReadingListRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - public void Update(ReadingList list) { - _context.Entry(list).State = EntityState.Modified; + context.Entry(list).State = EntityState.Modified; } public void Add(ReadingList list) { - _context.Add(list); + context.Add(list); } - public async Task Count() + public async Task Count(CancellationToken ct = default) { - return await _context.ReadingList.CountAsync(); + return await context.ReadingList.CountAsync(ct); } - public async Task GetCoverImageAsync(int readingListId) + public async Task GetCoverImageAsync(int readingListId, CancellationToken ct = default) { - return await _context.ReadingList + return await context.ReadingList .Where(c => c.Id == readingListId) .Select(c => c.CoverImage) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task> GetAllCoverImagesAsync() + public async Task> GetAllCoverImagesAsync(CancellationToken ct = default) { - return (await _context.ReadingList + return (await context.ReadingList .Select(t => t.CoverImage) .Where(t => !string.IsNullOrEmpty(t)) - .ToListAsync())!; + .ToListAsync(ct))!; } - public async Task> GetRandomCoverImagesAsync(int readingListId) + public async Task> GetRandomCoverImagesAsync(int readingListId, CancellationToken ct = default) { var random = new Random(); - var data = await _context.ReadingList + var data = await context.ReadingList .Where(r => r.Id == readingListId) .SelectMany(r => r.Items.Select(ri => ri.Chapter.CoverImage)) .Where(t => !string.IsNullOrEmpty(t)) - .ToListAsync(); + .ToListAsync(ct); return data .OrderBy(_ => random.Next()) @@ -118,46 +67,49 @@ public class ReadingListRepository : IReadingListRepository } - public async Task ReadingListExists(string name) + public async Task ReadingListExists(string name, int? readingListId = null, CancellationToken ct = default) { var normalized = name.ToNormalized(); - return await _context.ReadingList - .AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized)); + + return await context.ReadingList + .WhereIf(readingListId != null, x => x.Id != readingListId) + .AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized), ct); } - public async Task ReadingListExistsForUser(string name, int userId) + public async Task ReadingListExistsForUser(string name, int userId, CancellationToken ct = default) { var normalized = name.ToNormalized(); - return await _context.ReadingList - .AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized) && x.AppUserId == userId); + return await context.ReadingList + .AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized) && x.AppUserId == userId, ct); } - public IEnumerable GetReadingListPeopleAsync(int readingListId, PersonRole role) + public IEnumerable GetReadingListPeopleAsync(int readingListId, PersonRole role, + CancellationToken ct = default) { - return _context.ReadingListItem + return context.ReadingListItem .Where(item => item.ReadingListId == readingListId) .SelectMany(item => item.Chapter.People) .Where(p => p.Role == role) .OrderBy(p => p.Person.NormalizedName) .Select(p => p.Person) .Distinct() - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .AsEnumerable(); } - public async Task GetReadingListAllPeopleAsync(int readingListId) + public async Task GetReadingListAllPeopleAsync(int readingListId, CancellationToken ct = default) { - var allPeople = await _context.ReadingListItem + var allPeople = await context.ReadingListItem .Where(item => item.ReadingListId == readingListId) .SelectMany(item => item.Chapter.People) .OrderBy(p => p.Person.NormalizedName) .Select(p => new { - Role = p.Role, - Person = _mapper.Map(p.Person) + p.Role, + Person = mapper.Map(p.Person) }) .Distinct() - .ToListAsync(); + .ToListAsync(ct); // Create the ReadingListCast object var cast = new ReadingListCast(); @@ -216,62 +168,67 @@ public class ReadingListRepository : IReadingListRepository return cast; } - public async Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat) + public async Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, + CancellationToken ct = default) { var extension = encodeFormat.GetExtension(); - return await _context.ReadingList + return await context.ReadingList .Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension)) - .ToListAsync(); + .ToListAsync(ct); } - public async Task RemoveReadingListsWithoutSeries() + public async Task RemoveReadingListsWithoutSeries(CancellationToken ct = default) { - var listsToDelete = await _context.ReadingList + var listsToDelete = await context.ReadingList .Include(c => c.Items) .Where(c => c.Items.Count == 0) .AsSplitQuery() - .ToListAsync(); - _context.RemoveRange(listsToDelete); + .ToListAsync(ct); + context.RemoveRange(listsToDelete); - return await _context.SaveChangesAsync(); + return await context.SaveChangesAsync(ct); } - public async Task GetReadingListByTitleAsync(string name, int userId, ReadingListIncludes includes = ReadingListIncludes.Items) + public async Task GetReadingListByTitleAsync(string name, int userId, + ReadingListIncludes includes = ReadingListIncludes.Items, CancellationToken ct = default) { var normalized = name.ToNormalized(); - return await _context.ReadingList + return await context.ReadingList .Includes(includes) - .FirstOrDefaultAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized) && x.AppUserId == userId); + .FirstOrDefaultAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized) && x.AppUserId == userId, ct); } - public async Task> GetReadingListsByIds(IList ids, ReadingListIncludes includes = ReadingListIncludes.Items) + public async Task> GetReadingListsByIds(IList ids, + ReadingListIncludes includes = ReadingListIncludes.Items, CancellationToken ct = default) { - return await _context.ReadingList + return await context.ReadingList .Where(c => ids.Contains(c.Id)) .Includes(includes) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetReadingListsBySeriesId(int seriesId, ReadingListIncludes includes = ReadingListIncludes.Items) + public async Task> GetReadingListsBySeriesId(int seriesId, + ReadingListIncludes includes = ReadingListIncludes.Items, CancellationToken ct = default) { - return await _context.ReadingList + return await context.ReadingList .Where(rl => rl.Items.Any(rli => rli.SeriesId == seriesId)) .Includes(includes) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } /// /// Returns a Partial ReadingListInfoDto. The HourEstimate needs to be calculated outside the repo /// /// + /// /// - public async Task GetReadingListInfoAsync(int readingListId) + public async Task GetReadingListInfoAsync(int readingListId, CancellationToken ct = default) { - // Get sum of these across all ReadingListItems: long wordCount, int pageCount, bool isEpub (assume false if any ReadingListeItem.Series.Format is non-epub) - var readingList = await _context.ReadingList + // Get the sum of these across all ReadingListItems: long wordCount, int pageCount, bool isEpub (assume false if any ReadingListItem.Series.Format is non-epub) + var readingList = await context.ReadingList .Where(rl => rl.Id == readingListId) .Include(rl => rl.Items) .ThenInclude(item => item.Series) @@ -285,7 +242,7 @@ public class ReadingListRepository : IReadingListRepository Pages = rl.Items.Sum(item => item.Chapter.Pages), IsAllEpub = rl.Items.All(item => item.Series.Format == MangaFormat.Epub), }) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); return readingList; } @@ -293,104 +250,110 @@ public class ReadingListRepository : IReadingListRepository public void Remove(ReadingListItem item) { - _context.ReadingListItem.Remove(item); + context.ReadingListItem.Remove(item); } public void BulkRemove(IEnumerable items) { - _context.ReadingListItem.RemoveRange(items); + context.ReadingListItem.RemoveRange(items); } - public async Task> GetReadingListDtosForUserAsync(int userId, bool includePromoted, UserParams userParams, bool sortByLastModified = true) + public async Task> GetReadingListDtosForUserAsync(int userId, bool includePromoted, + UserParams userParams, bool sortByLastModified = true, CancellationToken ct = default) { - var user = await _context.AppUser.FirstAsync(u => u.Id == userId); - var query = _context.ReadingList + var user = await context.AppUser.FirstAsync(u => u.Id == userId, ct); + var query = context.ReadingList .Where(l => l.AppUserId == userId || (includePromoted && l.Promoted )) .RestrictAgainstAgeRestriction(user.GetAgeRestriction()); query = sortByLastModified ? query.OrderByDescending(l => l.LastModified) : query.OrderBy(l => l.Title); - var finalQuery = query.ProjectTo(_mapper.ConfigurationProvider) + var finalQuery = query.ProjectTo(mapper.ConfigurationProvider) .AsNoTracking(); - return await PagedList.CreateAsync(finalQuery, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(finalQuery, userParams.PageNumber, userParams.PageSize, ct); } - public async Task> GetReadingListDtosForSeriesAndUserAsync(int userId, int seriesId, bool includePromoted) + public async Task> GetReadingListDtosForSeriesAndUserAsync(int userId, int seriesId, + bool includePromoted, CancellationToken ct = default) { - var user = await _context.AppUser.FirstAsync(u => u.Id == userId); - var query = _context.ReadingList + var user = await context.AppUser.FirstAsync(u => u.Id == userId, ct); + var query = context.ReadingList .Where(l => l.AppUserId == userId || (includePromoted && l.Promoted )) .RestrictAgainstAgeRestriction(user.GetAgeRestriction()) .Where(l => l.Items.Any(i => i.SeriesId == seriesId)) .AsSplitQuery() .OrderBy(l => l.Title) - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .AsNoTracking(); - return await query.ToListAsync(); + return await query.ToListAsync(ct); } - public async Task> GetReadingListDtosForChapterAndUserAsync(int userId, int chapterId, bool includePromoted) + public async Task> GetReadingListDtosForChapterAndUserAsync(int userId, int chapterId, + bool includePromoted, CancellationToken ct = default) { - var user = await _context.AppUser.FirstAsync(u => u.Id == userId); - var query = _context.ReadingList + var user = await context.AppUser.FirstAsync(u => u.Id == userId, ct); + + var query = context.ReadingList .Where(l => l.AppUserId == userId || (includePromoted && l.Promoted )) .RestrictAgainstAgeRestriction(user.GetAgeRestriction()) .Where(l => l.Items.Any(i => i.ChapterId == chapterId)) .AsSplitQuery() .OrderBy(l => l.Title) - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .AsNoTracking(); - return await query.ToListAsync(); + return await query.ToListAsync(ct); } - public async Task GetReadingListByIdAsync(int readingListId, ReadingListIncludes includes = ReadingListIncludes.None) + public async Task GetReadingListByIdAsync(int readingListId, + ReadingListIncludes includes = ReadingListIncludes.None, CancellationToken ct = default) { - return await _context.ReadingList + return await context.ReadingList .Where(r => r.Id == readingListId) .Includes(includes) .Include(r => r.Items.OrderBy(item => item.Order)) .AsSplitQuery() - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } - public async Task AnyUserReadingProgressAsync(int readingListId, int userId) + public async Task AnyUserReadingProgressAsync(int readingListId, int userId, CancellationToken ct = default) { // Since the list is already created, we can assume RBS doesn't need to apply - var chapterIdsQuery = _context.ReadingListItem + var chapterIdsQuery = context.ReadingListItem .Where(s => s.ReadingListId == readingListId) .Select(s => s.ChapterId) .AsQueryable(); - return await _context.AppUserProgresses + return await context.AppUserProgresses .Where(p => chapterIdsQuery.Contains(p.ChapterId) && p.AppUserId == userId) .AsNoTracking() - .AnyAsync(); + .AnyAsync(ct); } - public async Task GetContinueReadingPoint(int readingListId, int userId) + public async Task GetContinueReadingPoint(int readingListId, int userId, + CancellationToken ct = default) { - var userLibraries = _context.Library.GetUserLibraries(userId); + var userLibraries = context.Library.GetUserLibraries(userId); - var query = _context.ReadingListItem + var query = context.ReadingListItem .Where(rli => rli.ReadingListId == readingListId) - .Join(_context.Chapter, rli => rli.ChapterId, chapter => chapter.Id, (rli, chapter) => new + .Join(context.Chapter, rli => rli.ChapterId, chapter => chapter.Id, (rli, chapter) => new { ReadingListItem = rli, Chapter = chapter, - FileSize = _context.MangaFile.Where(f => f.ChapterId == chapter.Id).Sum(f => (long?)f.Bytes) ?? 0 + FileSize = context.MangaFile.Where(f => f.ChapterId == chapter.Id).Sum(f => (long?)f.Bytes) ?? 0 }) - .Join(_context.Volume, x => x.ReadingListItem.VolumeId, volume => volume.Id, (x, volume) => new + .Join(context.Volume, x => x.ReadingListItem.VolumeId, volume => volume.Id, (x, volume) => new { x.ReadingListItem, x.Chapter, x.FileSize, Volume = volume }) - .Join(_context.Series, x => x.ReadingListItem.SeriesId, series => series.Id, (x, series) => new + .Join(context.Series, x => x.ReadingListItem.SeriesId, series => series.Id, (x, series) => new { x.ReadingListItem, x.Chapter, @@ -399,7 +362,7 @@ public class ReadingListRepository : IReadingListRepository Series = series }) .Where(x => userLibraries.Contains(x.Series.LibraryId)) - .GroupJoin(_context.AppUserProgresses.Where(p => p.AppUserId == userId), + .GroupJoin(context.AppUserProgresses.Where(p => p.AppUserId == userId), x => x.ReadingListItem.ChapterId, progress => progress.ChapterId, (x, progressGroup) => new @@ -428,20 +391,20 @@ public class ReadingListRepository : IReadingListRepository }) .OrderBy(x => x.ReadingListItem.Order); - // First try to find a partially read item then the first unread item + // First try to find a partially read item, then the first unread item var item = await query .OrderBy(x => x.IsPartiallyRead ? 0 : x.IsUnread ? 1 : 2) .ThenBy(x => x.ReadingListItem.Order) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); if (item == null) return null; // Map to DTO - var library = await _context.Library + var library = await context.Library .Where(l => l.Id == item.Series.LibraryId) .Select(l => new { l.Name, l.Type }) - .FirstAsync(); + .FirstAsync(ct); var dto = new ReadingListItemDto { @@ -472,21 +435,22 @@ public class ReadingListRepository : IReadingListRepository return dto; } - public Task GetReadingListItemCountAsync(int readingListId, int userId) + public Task GetReadingListItemCountAsync(int readingListId, int userId, CancellationToken ct = default) { - return _context.ReadingListItem.Where(rli => rli.ReadingListId == readingListId).CountAsync(); + return context.ReadingListItem.Where(rli => rli.ReadingListId == readingListId).CountAsync(ct); } - public async Task> GetReadingListItemDtosByIdAsync(int readingListId, int userId, UserParams? userParams = null) + public async Task> GetReadingListItemDtosByIdAsync(int readingListId, int userId, + UserParams? userParams = null, CancellationToken ct = default) { - var userLibraries = _context.Library.GetUserLibraries(userId); + var userLibraries = context.Library.GetUserLibraries(userId); - var query = _context.ReadingListItem + var query = context.ReadingListItem .Where(rli => rli.ReadingListId == readingListId) .Where(rli => userLibraries.Contains(rli.Series.LibraryId)) .OrderBy(rli => rli.Order) - .ProjectToWithProgress(_mapper, userId) + .ProjectToWithProgress(mapper, userId) .AsSplitQuery(); if (userParams != null) @@ -496,34 +460,38 @@ public class ReadingListRepository : IReadingListRepository .Take(userParams.PageSize); } - return await query.ToListAsync(); + return await query.ToListAsync(ct); } - public async Task GetReadingListDtoByIdAsync(int readingListId, int userId) + public async Task GetReadingListDtoByIdAsync(int readingListId, int userId, + CancellationToken ct = default) { - var user = await _context.AppUser.FirstAsync(u => u.Id == userId); - return await _context.ReadingList + var user = await context.AppUser.FirstAsync(u => u.Id == userId, ct); + + return await context.ReadingList .Where(r => r.Id == readingListId && (r.AppUserId == userId || r.Promoted)) .RestrictAgainstAgeRestriction(user.GetAgeRestriction()) - .ProjectTo(_mapper.ConfigurationProvider) - .SingleOrDefaultAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .SingleOrDefaultAsync(ct); } - public async Task GetReadingListDtoByTitleAsync(int userId, string title) + public async Task GetReadingListDtoByTitleAsync(int userId, string title, + CancellationToken ct = default) { - return await _context.ReadingList + return await context.ReadingList .Where(r => r.Title.Equals(title) && r.AppUserId == userId) - .ProjectTo(_mapper.ConfigurationProvider) - .SingleOrDefaultAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .SingleOrDefaultAsync(ct); } - public async Task> GetReadingListItemsByIdAsync(int readingListId) + public async Task> GetReadingListItemsByIdAsync(int readingListId, + CancellationToken ct = default) { - return await _context.ReadingListItem + return await context.ReadingListItem .Where(r => r.ReadingListId == readingListId) .OrderBy(r => r.Order) - .ToListAsync(); + .ToListAsync(ct); } diff --git a/API/Data/Repositories/ReadingSessionRepository.cs b/Kavita.Database/Repositories/ReadingSessionRepository.cs similarity index 83% rename from API/Data/Repositories/ReadingSessionRepository.cs rename to Kavita.Database/Repositories/ReadingSessionRepository.cs index 26b4da900..5d3ececec 100644 --- a/API/Data/Repositories/ReadingSessionRepository.cs +++ b/Kavita.Database/Repositories/ReadingSessionRepository.cs @@ -1,21 +1,19 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.DTOs.Progress; using AutoMapper; +using Kavita.API.Repositories; +using Kavita.Models.DTOs.Progress; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; - -public interface IReadingSessionRepository -{ - Task> GetAllReadingSessionAsync(bool isActiveOnly = true); -} +namespace Kavita.Database.Repositories; public class ReadingSessionRepository(DataContext context, IMapper mapper) : IReadingSessionRepository { - public async Task> GetAllReadingSessionAsync(bool isActiveOnly = true) + public async Task> GetAllReadingSessionAsync(bool isActiveOnly = true, + CancellationToken ct = default) { var query = context.AppUserReadingSession .Where(s => !isActiveOnly || s.IsActive); @@ -23,7 +21,7 @@ public class ReadingSessionRepository(DataContext context, IMapper mapper) : IRe var sessions = await query .Include(s => s.ActivityData) .Include(s => s.AppUser) - .ToListAsync(); + .ToListAsync(ct); if (sessions.Count == 0) return []; @@ -37,18 +35,18 @@ public class ReadingSessionRepository(DataContext context, IMapper mapper) : IRe var seriesIds = allActivityData.Select(a => a.SeriesId).Distinct().ToList(); var chapterIds = allActivityData.Select(a => a.ChapterId).Distinct().ToList(); - // Fetch all lookups in parallel - single query per table + // Fetch all lookups in a parallel - single query per table var libraryLookupTask = context.Library .Where(l => libraryIds.Contains(l.Id)) - .ToDictionaryAsync(l => l.Id, l => l.Name); + .ToDictionaryAsync(l => l.Id, l => l.Name, ct); var seriesLookupTask = context.Series .Where(s => seriesIds.Contains(s.Id)) - .ToDictionaryAsync(s => s.Id, s => s.Name); + .ToDictionaryAsync(s => s.Id, s => s.Name, ct); var chapterLookupTask = context.Chapter .Where(c => chapterIds.Contains(c.Id)) - .ToDictionaryAsync(c => c.Id, c => c.TitleName); + .ToDictionaryAsync(c => c.Id, c => c.TitleName, ct); await Task.WhenAll(libraryLookupTask, seriesLookupTask, chapterLookupTask); diff --git a/Kavita.Database/Repositories/ScrobbleEventRepository.cs b/Kavita.Database/Repositories/ScrobbleEventRepository.cs new file mode 100644 index 000000000..90e3a5a26 --- /dev/null +++ b/Kavita.Database/Repositories/ScrobbleEventRepository.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Common.Helpers; +using Kavita.Database.Extensions; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.Entities.Scrobble; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Repositories; + +/// +/// This handles everything around Scrobbling +/// +public class ScrobbleRepository(DataContext context, IMapper mapper) : IScrobbleRepository +{ + public void Attach(ScrobbleEvent evt) + { + context.ScrobbleEvent.Attach(evt); + } + + public void Attach(ScrobbleError error) + { + context.ScrobbleError.Attach(error); + } + + public void Remove(ScrobbleEvent evt) + { + context.ScrobbleEvent.Remove(evt); + } + + public void Remove(IEnumerable events) + { + context.ScrobbleEvent.RemoveRange(events); + } + + public void Remove(IEnumerable errors) + { + context.ScrobbleError.RemoveRange(errors); + } + + public void Update(ScrobbleEvent evt) + { + context.Entry(evt).State = EntityState.Modified; + } + + public async Task> GetByEvent(ScrobbleEventType type, bool isProcessed = false, + CancellationToken ct = default) + { + return await context.ScrobbleEvent + .Include(s => s.Series) + .ThenInclude(s => s.Library) + .Include(s => s.Series) + .ThenInclude(s => s.Metadata) + .Include(s => s.AppUser) + .ThenInclude(u => u.UserPreferences) + .Where(s => s.ScrobbleEventType == type) + .Where(s => s.IsProcessed == isProcessed) + .AsSplitQuery() + .GroupBy(s => s.SeriesId) + .Select(g => g.OrderByDescending(e => e.ChapterNumber) + .ThenByDescending(e => e.VolumeNumber) + .First()) + .ToListAsync(ct); + } + + /// + /// Returns all processed events processed 7 or more days ago + /// + /// + /// + /// + public async Task> GetProcessedEvents(int daysAgo, CancellationToken ct = default) + { + var date = DateTime.UtcNow.Subtract(TimeSpan.FromDays(daysAgo)); + return await context.ScrobbleEvent + .Where(s => s.IsProcessed) + .Where(s => s.ProcessDateUtc != null && s.ProcessDateUtc < date) + .ToListAsync(ct); + } + + public async Task Exists(int userId, int seriesId, ScrobbleEventType eventType, CancellationToken ct = default) + { + return await context.ScrobbleEvent.AnyAsync(e => + e.AppUserId == userId && e.SeriesId == seriesId && e.ScrobbleEventType == eventType, ct); + } + + public async Task> GetScrobbleErrors(CancellationToken ct = default) + { + return await context.ScrobbleError + .OrderBy(e => e.LastModifiedUtc) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); + } + + public async Task> GetAllScrobbleErrorsForSeries(int seriesId, CancellationToken ct = default) + { + return await context.ScrobbleError + .Where(e => e.SeriesId == seriesId) + .ToListAsync(ct); + } + + public async Task ClearScrobbleErrors(CancellationToken ct = default) + { + context.ScrobbleError.RemoveRange(context.ScrobbleError); + await context.SaveChangesAsync(ct); + } + + public async Task HasErrorForSeries(int seriesId, CancellationToken ct = default) + { + return await context.ScrobbleError.AnyAsync(n => n.SeriesId == seriesId, ct); + } + + public async Task GetEvent(int userId, int seriesId, ScrobbleEventType eventType, + bool isNotProcessed = false, CancellationToken ct = default) + { + return await context.ScrobbleEvent + .Where(e => e.AppUserId == userId && e.SeriesId == seriesId && e.ScrobbleEventType == eventType) + .WhereIf(isNotProcessed, e => !e.IsProcessed) + .OrderBy(e => e.LastModifiedUtc) + .FirstOrDefaultAsync(ct); + } + + public async Task> GetUserEventsForSeries(int userId, int seriesId, + CancellationToken ct = default) + { + return await context.ScrobbleEvent + .Where(e => e.AppUserId == userId && !e.IsProcessed && e.SeriesId == seriesId) + .Include(e => e.Series) + .OrderBy(e => e.LastModifiedUtc) + .AsSplitQuery() + .ToListAsync(ct); + } + + public async Task> GetUserEvents(int userId, IList scrobbleEventIds, + CancellationToken ct = default) + { + return await context.ScrobbleEvent + .Where(e => e.AppUserId == userId && scrobbleEventIds.Contains(e.Id)) + .ToListAsync(ct); + } + + public async Task> GetUserEvents(int userId, ScrobbleEventFilter filter, + UserParams pagination, CancellationToken ct = default) + { + var query = context.ScrobbleEvent + .Where(e => e.AppUserId == userId) + .Include(e => e.Series) + .WhereIf(!string.IsNullOrEmpty(filter.Query), s => + EF.Functions.Like(s.Series.Name, $"%{filter.Query}%") + ) + .WhereIf(!filter.IncludeReviews, e => e.ScrobbleEventType != ScrobbleEventType.Review) + .SortBy(filter.Field, filter.IsDescending) + .AsSplitQuery() + .ProjectTo(mapper.ConfigurationProvider); + + return await PagedList.CreateAsync(query, pagination.PageNumber, pagination.PageSize, ct); + } + + public async Task> GetAllEventsForSeries(int seriesId, CancellationToken ct = default) + { + return await context.ScrobbleEvent + .Where(e => e.SeriesId == seriesId) + .ToListAsync(ct); + } + + public async Task> GetAllEventsWithSeriesIds(IEnumerable seriesIds, + CancellationToken ct = default) + { + return await context.ScrobbleEvent + .Where(e => seriesIds.Contains(e.SeriesId)) + .ToListAsync(ct); + } + + public async Task> GetEvents(CancellationToken ct = default) + { + return await context.ScrobbleEvent + .Include(e => e.AppUser) + .ToListAsync(ct); + } +} diff --git a/Kavita.Database/Repositories/SeriesMetadataRepository.cs b/Kavita.Database/Repositories/SeriesMetadataRepository.cs new file mode 100644 index 000000000..57bc17df6 --- /dev/null +++ b/Kavita.Database/Repositories/SeriesMetadataRepository.cs @@ -0,0 +1,14 @@ +using Kavita.API.Repositories; +using Kavita.Models.Entities.Metadata; + +namespace Kavita.Database.Repositories; + + + +public class SeriesMetadataRepository(DataContext context) : ISeriesMetadataRepository +{ + public void Update(SeriesMetadata seriesMetadata) + { + context.SeriesMetadata.Update(seriesMetadata); + } +} diff --git a/API/Data/Repositories/SeriesRepository.cs b/Kavita.Database/Repositories/SeriesRepository.cs similarity index 70% rename from API/Data/Repositories/SeriesRepository.cs rename to Kavita.Database/Repositories/SeriesRepository.cs index d461fa386..c4642696f 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/Kavita.Database/Repositories/SeriesRepository.cs @@ -3,213 +3,82 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; -using API.Constants; -using API.Data.Misc; -using API.Data.Scanner; -using API.DTOs; -using API.DTOs.Collection; -using API.DTOs.Dashboard; -using API.DTOs.Filtering; -using API.DTOs.Filtering.v2; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Metadata; -using API.DTOs.Person; -using API.DTOs.Reader; -using API.DTOs.ReadingLists; -using API.DTOs.Scrobbling; -using API.DTOs.Search; -using API.DTOs.SeriesDetail; -using API.DTOs.Settings; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Extensions; -using API.Extensions.QueryExtensions; -using API.Extensions.QueryExtensions.Filtering; -using API.Helpers; -using API.Helpers.Converters; -using API.Services; -using API.Services.Plus; -using API.Services.Reading; -using API.Services.Tasks; -using API.Services.Tasks.Scanner; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Database.Converters; +using Kavita.Database.Extensions; +using Kavita.Database.Extensions.Filters; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Collection; +using Kavita.Models.DTOs.Dashboard; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.DTOs.Search; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.User; +using Kavita.Models.Extensions; +using Kavita.Models.Misc; +using Kavita.Models.Parser; using Microsoft.EntityFrameworkCore; +using YamlDotNet.Core; -namespace API.Data.Repositories; -#nullable enable +namespace Kavita.Database.Repositories; -[Flags] -public enum SeriesIncludes +public class SeriesRepository(DataContext context, IMapper mapper) : ISeriesRepository { - None = 1, - Volumes = 2, - /// - /// This will include all necessary includes - /// - Metadata = 4, - Related = 8, - Library = 16, - Chapters = 32, - ExternalReviews = 64, - ExternalRatings = 128, - ExternalRecommendations = 256, - ExternalMetadata = 512, - - ExternalData = ExternalMetadata | ExternalReviews | ExternalRatings | ExternalRecommendations, -} - -/// -/// For complex queries, Library has certain restrictions where the library should not be included in results. -/// This enum dictates which field to use for the lookup. -/// -public enum QueryContext -{ - None = 1, - Search = 2, - [Obsolete("Use Dashboard")] - Recommended = 3, - Dashboard = 4, -} - -public interface ISeriesRepository -{ - void Add(Series series); - void Attach(SeriesRelation relation); - void Update(Series series); - void Update(SeriesMetadata seriesMetadata); - void Remove(Series series); - void Remove(IEnumerable series); - Task DoesSeriesNameExistInLibrary(string name, int libraryId, MangaFormat format); - /// - /// Adds user information like progress, ratings, etc - /// - /// - /// - /// Pagination info - /// Filtering/Sorting to apply - /// - Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams, FilterDto filter); - /// - /// Does not add user information like progress, ratings, etc. - /// - /// - /// - /// - /// - /// Includes Files in the Search - /// - Task SearchSeries(int userId, bool isAdmin, IList libraryIds, string searchQuery, bool includeChapterAndFiles = true); - 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, bool fullSeries = true); - Task GetChapterIdsForSeriesAsync(IList seriesIds); - Task>> GetChapterIdWithSeriesIdForSeriesAsync(int[] seriesIds); - Task GetSeriesCoverImageAsync(int seriesId); - Task> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto? filter); - Task> GetRecentlyAdded(int libraryId, int userId, UserParams userParams, FilterDto filter); - Task> GetRecentlyAddedV2(int userId, UserParams userParams, FilterV2Dto filter); - Task GetSeriesMetadata(int seriesId); - Task> GetSeriesDtoForCollectionAsync(int collectionId, int userId, UserParams userParams); - Task> GetFilesForSeries(int seriesId); - Task GetFilesizeForSeriesAsync(int seriesId); - Task> GetFilesizeForMultipleSeriesAsync(IList seriesIds); - Task> GetSeriesDtoForIdsAsync(IEnumerable seriesIds, int userId); - Task> GetAllCoverImagesAsync(); - Task> GetLockedCoverImagesAsync(); - Task> GetFullSeriesForLibraryIdAsync(int libraryId, UserParams userParams); - Task GetFullSeriesForSeriesIdAsync(int seriesId); - Task GetChunkInfo(int libraryId = 0); - Task> GetRecentlyUpdatedSeries(int userId, UserParams? userParams); - Task GetRelatedSeries(int userId, int seriesId); - Task> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind); - Task> GetQuickReads(int userId, int libraryId, UserParams userParams); - Task> GetQuickCatchupReads(int userId, int libraryId, UserParams userParams); - Task> GetHighlyRated(int userId, int libraryId, UserParams userParams); - Task> GetMoreIn(int userId, int libraryId, int genreId, UserParams userParams); - Task> GetRediscover(int userId, int libraryId, UserParams userParams); - Task GetSeriesForMangaFile(int mangaFileId, int userId); - Task GetSeriesForChapter(int chapterId, int userId); - Task> GetWantToReadForUserAsync(int userId, UserParams userParams, FilterDto filter); - Task> GetWantToReadForUserV2Async(int userId, UserParams userParams, FilterV2Dto filter); - Task> GetWantToReadForUserAsync(int userId); - Task IsSeriesInWantToRead(int userId, int seriesId); - Task GetSeriesByFolderPath(string folder, SeriesIncludes includes = SeriesIncludes.None); - Task GetSeriesThatContainsLowestFolderPath(string path, SeriesIncludes includes = SeriesIncludes.None); - Task> GetAllSeriesByNameAsync(IList normalizedNames, - int userId, SeriesIncludes includes = SeriesIncludes.None); - Task GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, MangaFormat format, bool withFullIncludes = true); - Task GetSeriesByAnyName(IList names, IList formats, - int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None); - Task GetSeriesByAnyName(string seriesName, string localizedName, IList formats, int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None); - 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); - Task> GetSeriesMetadataForIds(IEnumerable seriesIds); - Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, bool customOnly = true); - 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, QueryContext queryContext = QueryContext.None); - Task GetPlusSeriesDto(int seriesId); - Task MatchSeries(ExternalSeriesDetailDto externalSeries); -} - -public class SeriesRepository : ISeriesRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - private readonly Regex _yearRegex = new(@"\d{4}", RegexOptions.Compiled, - Services.Tasks.Scanner.Parser.Parser.RegexTimeout); - - public SeriesRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } + private readonly Regex _yearRegex = new(@"\d{4}", RegexOptions.Compiled, TimeSpan.FromMilliseconds(500)); public void Add(Series series) { - _context.Series.Add(series); + context.Series.Add(series); } public void Attach(SeriesRelation relation) { - _context.SeriesRelation.Attach(relation); + context.SeriesRelation.Attach(relation); } public void Attach(ExternalSeriesMetadata metadata) { - _context.ExternalSeriesMetadata.Attach(metadata); + context.ExternalSeriesMetadata.Attach(metadata); } public void Update(Series series) { - _context.Entry(series).State = EntityState.Modified; + context.Entry(series).State = EntityState.Modified; } public void Update(SeriesMetadata seriesMetadata) { - _context.Entry(seriesMetadata).State = EntityState.Modified; + context.Entry(seriesMetadata).State = EntityState.Modified; } public void Remove(Series series) { - _context.Series.Remove(series); + context.Series.Remove(series); } public void Remove(IEnumerable series) { - _context.Series.RemoveRange(series); + context.Series.RemoveRange(series); } /// @@ -218,23 +87,26 @@ public class SeriesRepository : ISeriesRepository /// Name of series /// /// Format of series + /// /// - public async Task DoesSeriesNameExistInLibrary(string name, int libraryId, MangaFormat format) + public async Task DoesSeriesNameExistInLibrary(string name, int libraryId, MangaFormat format, + CancellationToken ct = default) { - return await _context.Series + return await context.Series .AsNoTracking() .Where(s => s.LibraryId == libraryId && s.Name.Equals(name) && s.Format == format) - .AnyAsync(); + .AnyAsync(ct); } - public async Task> GetSeriesForLibraryIdAsync(int libraryId, SeriesIncludes includes = SeriesIncludes.None) + public async Task> GetSeriesForLibraryIdAsync(int libraryId, + SeriesIncludes includes = SeriesIncludes.None, CancellationToken ct = default) { - return await _context.Series + return await context.Series .Where(s => s.LibraryId == libraryId) .Includes(includes) .OrderBy(s => s.SortName.ToLower()) - .ToListAsync(); + .ToListAsync(ct); } /// @@ -242,11 +114,13 @@ public class SeriesRepository : ISeriesRepository /// /// /// + /// /// - public async Task> GetFullSeriesForLibraryIdAsync(int libraryId, UserParams userParams) + public async Task> GetFullSeriesForLibraryIdAsync(int libraryId, UserParams userParams, + CancellationToken ct = default) { - #nullable disable - var query = _context.Series +#nullable disable + var query = context.Series .Where(s => s.LibraryId == libraryId) .Include(s => s.Metadata) @@ -280,18 +154,19 @@ public class SeriesRepository : ISeriesRepository .OrderBy(s => s.SortName.ToLower()); #nullable enable - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize, ct); } /// /// This is a heavy call. Returns all entities down to Files and Library and Series Metadata. /// /// + /// /// - public async Task GetFullSeriesForSeriesIdAsync(int seriesId) + public async Task GetFullSeriesForSeriesIdAsync(int seriesId, CancellationToken ct = default) { - #nullable disable - return await _context.Series +#nullable disable + return await context.Series .Where(s => s.Id == seriesId) .Include(s => s.Relations) .Include(s => s.Metadata) @@ -319,8 +194,8 @@ public class SeriesRepository : ISeriesRepository .ThenInclude(v => v.Chapters) .ThenInclude(c => c.Files) .AsSplitQuery() - .SingleOrDefaultAsync(); - #nullable enable + .SingleOrDefaultAsync(ct); +#nullable enable } /// @@ -330,89 +205,92 @@ public class SeriesRepository : ISeriesRepository /// /// /// + /// /// [Obsolete("Use GetSeriesDtoForLibraryIdAsync")] - public async Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams, FilterDto filter) + public async Task> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, + UserParams userParams, FilterDto filter, CancellationToken ct = default) { - var query = await CreateFilteredSearchQueryable(userId, libraryId, filter, QueryContext.None); + var query = await CreateFilteredSearchQueryable(userId, libraryId, filter, QueryContext.None, ct); var retSeries = query - .ProjectToWithProgress(_mapper, userId) + .ProjectToWithProgress(mapper, userId) .AsSplitQuery() .AsNoTracking(); - return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize, ct); } - private async Task> GetUserLibrariesForFilteredQuery(int libraryId, int userId, QueryContext queryContext) + private async Task> GetUserLibrariesForFilteredQuery(int libraryId, int userId, QueryContext queryContext, CancellationToken ct = default) { if (libraryId == 0) { - return await _context.Library.GetUserLibraries(userId, queryContext).ToListAsync(); + return await context.Library.GetUserLibraries(userId, queryContext).ToListAsync(ct); } return [libraryId]; } - public async Task SearchSeries(int userId, bool isAdmin, IList libraryIds, string searchQuery, bool includeChapterAndFiles = true) + public async Task SearchSeries(int userId, bool isAdmin, IList libraryIds, + string searchQuery, bool includeChapterAndFiles = true, CancellationToken ct = default) { const int maxRecords = 15; var searchQueryNormalized = searchQuery.ToNormalized(); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); var justYear = _yearRegex.Match(searchQuery).Value; var hasYearInQuery = !string.IsNullOrEmpty(justYear); var yearComparison = hasYearInQuery ? int.Parse(justYear) : 0; - var baseSeriesQuery = _context.Series + var baseSeriesQuery = context.Series .Where(s => libraryIds.Contains(s.LibraryId)) .RestrictAgainstAgeRestriction(userRating); #region Independent Queries - var librariesTask = _context.Library + var librariesTask = context.Library .Search(searchQuery, userId, libraryIds) .Take(maxRecords) .OrderBy(l => l.Name.ToLower()) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); - var annotationsTask = _context.AppUserAnnotation + var annotationsTask = context.AppUserAnnotation .Where(a => a.AppUserId == userId && (EF.Functions.Like(a.Comment, $"%{searchQueryNormalized}%") || EF.Functions.Like(a.Context, $"%{searchQueryNormalized}%"))) .Take(maxRecords) .OrderBy(l => l.CreatedUtc) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); #endregion var seriesTask = baseSeriesQuery - .Where(s => EF.Functions.Like(s.Name, $"%{searchQuery}%") - || (s.OriginalName != null && EF.Functions.Like(s.OriginalName, $"%{searchQuery}%")) - || (s.LocalizedName != null && EF.Functions.Like(s.LocalizedName, $"%{searchQuery}%")) - || EF.Functions.Like(s.NormalizedName, $"%{searchQueryNormalized}%") - || (hasYearInQuery && s.Metadata.ReleaseYear == yearComparison)) - .OrderBy(s => s.SortName!.Length) - .ThenBy(s => s.SortName!.ToLower()) - .Take(maxRecords) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .Where(s => EF.Functions.Like(s.Name, $"%{searchQuery}%") + || (s.OriginalName != null && EF.Functions.Like(s.OriginalName, $"%{searchQuery}%")) + || (s.LocalizedName != null && EF.Functions.Like(s.LocalizedName, $"%{searchQuery}%")) + || EF.Functions.Like(s.NormalizedName, $"%{searchQueryNormalized}%") + || (hasYearInQuery && s.Metadata.ReleaseYear == yearComparison)) + .OrderBy(s => s.SortName!.Length) + .ThenBy(s => s.SortName!.ToLower()) + .Take(maxRecords) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); - var readingListsTask = _context.ReadingList + var readingListsTask = context.ReadingList .Search(searchQuery, userId, userRating) .Take(maxRecords) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); - var collectionsTask = _context.AppUserCollection + var collectionsTask = context.AppUserCollection .Search(searchQuery, userId, userRating) .Take(maxRecords) .OrderBy(c => c.NormalizedTitle) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); - var bookmarksTask = _context.AppUserBookmark + var bookmarksTask = context.AppUserBookmark .Where(b => b.AppUserId == userId) .Where(b => libraryIds.Contains(b.Series.LibraryId)) .Where(b => EF.Functions.Like(b.Series.Name, $"%{searchQuery}%") || @@ -431,40 +309,40 @@ public class SeriesRepository : ISeriesRepository VolumeId = g.First().VolumeId }) .Take(maxRecords) - .ToListAsync(); + .ToListAsync(ct); var seriesIdsSubquery = baseSeriesQuery.Select(s => s.Id); - var personsTask = _context.Person - .Where(p => _context.SeriesMetadataPeople + var personsTask = context.Person + .Where(p => context.SeriesMetadataPeople .Any(smp => smp.PersonId == p.Id && seriesIdsSubquery.Contains(smp.SeriesMetadata.SeriesId) && (EF.Functions.Like(p.NormalizedName, $"%{searchQueryNormalized}%") || p.Aliases.Any(a => EF.Functions.Like(a.NormalizedAlias, $"%{searchQueryNormalized}%")) - ))) + ))) .OrderBy(p => p.NormalizedName.Length) .ThenBy(p => p.NormalizedName) .Take(maxRecords) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); - var genresTask = _context.Genre - .Where(g => _context.SeriesMetadata - .Any(sm => seriesIdsSubquery.Contains(sm.SeriesId) && - sm.Genres.Any(sg => sg.Id == g.Id)) && - EF.Functions.Like(g.NormalizedTitle, $"%{searchQueryNormalized}%")) + var genresTask = context.Genre + .Where(g => context.SeriesMetadata + .Any(sm => seriesIdsSubquery.Contains(sm.SeriesId) && + sm.Genres.Any(sg => sg.Id == g.Id)) && + EF.Functions.Like(g.NormalizedTitle, $"%{searchQueryNormalized}%")) .Take(maxRecords) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); - var tagsTask = _context.Tag - .Where(t => _context.SeriesMetadata - .Any(sm => seriesIdsSubquery.Contains(sm.SeriesId) && - sm.Tags.Any(st => st.Id == t.Id)) && - EF.Functions.Like(t.NormalizedTitle, $"%{searchQueryNormalized}%")) + var tagsTask = context.Tag + .Where(t => context.SeriesMetadata + .Any(sm => seriesIdsSubquery.Contains(sm.SeriesId) && + sm.Tags.Any(st => st.Id == t.Id)) && + EF.Functions.Like(t.NormalizedTitle, $"%{searchQueryNormalized}%")) .Take(maxRecords) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); // Run separate DB queries in parallel await Task.WhenAll( @@ -489,7 +367,7 @@ public class SeriesRepository : ISeriesRepository if (includeChapterAndFiles) { // Use EXISTS subquery pattern instead of loading IDs - var chaptersQuery = _context.Chapter + var chaptersQuery = context.Chapter .Where(c => c.Volume.Series.LibraryId > 0 && // Ensure navigation works libraryIds.Contains(c.Volume.Series.LibraryId)) .Where(c => EF.Functions.Like(c.TitleName, $"%{searchQuery}%") @@ -504,19 +382,19 @@ public class SeriesRepository : ISeriesRepository .OrderBy(c => c.TitleName.Length) .ThenBy(c => c.TitleName) .Take(maxRecords) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); if (isAdmin) { - result.Files = await _context.MangaFile + result.Files = await context.MangaFile .Where(f => EF.Functions.Like(f.FilePath, $"%{searchQuery}%")) .Where(f => libraryIds.Contains(f.Chapter.Volume.Series.LibraryId)) .Where(f => baseSeriesQuery.Any(s => s.Id == f.Chapter.Volume.SeriesId)) .OrderBy(f => f.FilePath) .Take(maxRecords) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } } @@ -529,12 +407,12 @@ public class SeriesRepository : ISeriesRepository /// /// /// - public async Task GetSeriesDtoByIdAsync(int seriesId, int userId) + public async Task GetSeriesDtoByIdAsync(int seriesId, int userId, CancellationToken ct = default) { - var series = await _context.Series + var series = await context.Series .Where(x => x.Id == seriesId) - .ProjectToWithProgress(_mapper, userId) - .SingleOrDefaultAsync(); + .ProjectToWithProgress(mapper, userId) + .SingleOrDefaultAsync(ct); return series ?? null; } @@ -544,13 +422,15 @@ public class SeriesRepository : ISeriesRepository /// /// /// + /// /// - public async Task GetSeriesByIdAsync(int seriesId, SeriesIncludes includes = SeriesIncludes.Volumes | SeriesIncludes.Metadata) + public async Task GetSeriesByIdAsync(int seriesId, + SeriesIncludes includes = SeriesIncludes.Metadata | SeriesIncludes.Volumes, CancellationToken ct = default) { - return await _context.Series + return await context.Series .Where(s => s.Id == seriesId) .Includes(includes) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } /// @@ -559,21 +439,21 @@ public class SeriesRepository : ISeriesRepository /// /// Include all the includes or just the Series /// - public async Task> GetSeriesByIdsAsync(IList seriesIds, bool fullSeries = true) + public async Task> GetSeriesByIdsAsync(IList seriesIds, bool fullSeries = true, CancellationToken ct = default) { - var query = _context.Series + var query = context.Series .Where(s => seriesIds.Contains(s.Id)) .AsSplitQuery(); - if (!fullSeries) return await query.ToListAsync(); + if (!fullSeries) return await query.ToListAsync(ct); return await query .Include(s => s.Volumes) - .ThenInclude(v => v.Chapters) - .ThenInclude(c => c.ExternalRatings) + .ThenInclude(v => v.Chapters) + .ThenInclude(c => c.ExternalRatings) .Include(s => s.Volumes) - .ThenInclude(v => v.Chapters) - .ThenInclude(c => c.ExternalReviews) + .ThenInclude(v => v.Chapters) + .ThenInclude(c => c.ExternalReviews) .Include(s => s.Relations) .Include(s => s.Metadata) @@ -585,36 +465,37 @@ public class SeriesRepository : ISeriesRepository .ThenInclude(e => e.ExternalReviews) .Include(s => s.ExternalSeriesMetadata) .ThenInclude(e => e.ExternalRecommendations) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetSeriesDtoByIdsAsync(IEnumerable seriesIds, AppUser user) + public async Task> GetSeriesDtoByIdsAsync(IEnumerable seriesIds, AppUser user, + CancellationToken ct = default) { - var allowedLibraries = await _context.Library + var allowedLibraries = await context.Library .Where(library => library.AppUsers.Any(x => x.Id == user.Id)) .Select(l => l.Id) - .ToListAsync(); + .ToListAsync(ct); var restriction = new AgeRestriction() { AgeRating = user.AgeRestriction, IncludeUnknowns = user.AgeRestrictionIncludeUnknowns }; - return await _context.Series + return await context.Series .Include(s => s.Metadata) .Where(s => seriesIds.Contains(s.Id) && allowedLibraries.Contains(s.LibraryId)) .RestrictAgainstAgeRestriction(restriction) .AsSplitQuery() - .ProjectToWithProgress(_mapper, user.Id) - .ToListAsync(); + .ProjectToWithProgress(mapper, user.Id) + .ToListAsync(ct); } - public async Task GetChapterIdsForSeriesAsync(IList seriesIds) + public async Task GetChapterIdsForSeriesAsync(IList seriesIds, CancellationToken ct = default) { - var volumes = await _context.Volume + var volumes = await context.Volume .Where(v => seriesIds.Contains(v.SeriesId)) .Include(v => v.Chapters) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); IList chapterIds = new List(); foreach (var v in volumes) @@ -632,14 +513,16 @@ public class SeriesRepository : ISeriesRepository /// This returns a dictionary mapping seriesId -> list of chapters back for each series id passed /// /// + /// /// - public async Task>> GetChapterIdWithSeriesIdForSeriesAsync(int[] seriesIds) + public async Task>> GetChapterIdWithSeriesIdForSeriesAsync(int[] seriesIds, + CancellationToken ct = default) { - var volumes = await _context.Volume + var volumes = await context.Volume .Where(v => seriesIds.Contains(v.SeriesId)) .Include(v => v.Chapters) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); var seriesChapters = new Dictionary>(); foreach (var v in volumes) @@ -658,18 +541,42 @@ public class SeriesRepository : ISeriesRepository return seriesChapters; } - public async Task> GetSeriesMetadataForIds(IEnumerable seriesIds) + public async Task GetFilesizeForSeriesAsync(int seriesId, CancellationToken ct = default) { - return await _context.SeriesMetadata + return await context.Volume + .Where(v => v.SeriesId == seriesId) + .SumAsync(v => v.Chapters.Sum(c => c.Files.Sum(f => f.Bytes)), cancellationToken: ct); + } + + public async Task> GetFilesizeForMultipleSeriesAsync(IList seriesIds, CancellationToken ct = default) + { + return await seriesIds.BatchToDictionaryAsync(50, batch => + context.Volume + .Where(v => batch.Contains(v.SeriesId)) + .GroupBy(v => v.SeriesId) + .Select(g => new + { + SeriesId = g.Key, + TotalBytes = g.SelectMany(v => v.Chapters) + .SelectMany(c => c.Files) + .Sum(f => f.Bytes) + }) + .ToDictionaryAsync(x => x.SeriesId, x => x.TotalBytes, cancellationToken: ct)); + } + + public async Task> GetSeriesMetadataForIds(IEnumerable seriesIds, + CancellationToken ct = default) + { + return await context.SeriesMetadata .Where(metadata => seriesIds.Contains(metadata.SeriesId)) .Include(m => m.Genres.OrderBy(g => g.NormalizedTitle)) .Include(m => m.Tags.OrderBy(g => g.NormalizedTitle)) .Include(m => m.People) .ThenInclude(p => p.Person) .AsNoTracking() - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } /// @@ -678,11 +585,11 @@ public class SeriesRepository : ISeriesRepository /// If customOnly, this will not include any volumes/chapters /// public async Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, - bool customOnly = true) + bool customOnly = true, CancellationToken ct = default) { var extension = encodeFormat.GetExtension(); - var prefix = ImageService.GetSeriesFormat(0).Replace("0", string.Empty); - var query = _context.Series + var prefix = "series{0}".Replace("0", string.Empty); // default: This actually depends on ImageService#GetSeriesFormat + var query = context.Series .Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension) && (!customOnly || c.CoverImage.StartsWith(prefix))) @@ -694,24 +601,25 @@ public class SeriesRepository : ISeriesRepository .ThenInclude(v => v.Chapters); } - return await query.ToListAsync(); + return await query.ToListAsync(ct); } - public async Task> GetSeriesDtoForLibraryIdV2Async(int userId, UserParams userParams, FilterV2Dto filterDto, QueryContext queryContext = QueryContext.None) + public async Task> GetSeriesDtoForLibraryIdV2Async(int userId, UserParams userParams, + FilterV2Dto filterDto, QueryContext queryContext = QueryContext.None, CancellationToken ct = default) { - var query = await CreateFilteredSearchQueryableV2(userId, filterDto, queryContext); + var query = await CreateFilteredSearchQueryableV2(userId, filterDto, queryContext, ct: ct); - var retSeries = query.ProjectToWithProgress(_mapper, userId); + var retSeries = query.ProjectToWithProgress(mapper, userId); - return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize, ct); } - public async Task GetPlusSeriesDto(int seriesId) + public async Task GetPlusSeriesDto(int seriesId, CancellationToken ct = default) { // I need to check Weblinks when AniListId/MalId is already set in ExternalSeries // Updating stale data should prioritize ExternalSeriesMetadata before Weblinks, to prioritize prior matches - var result = await _context.Series + var result = await context.Series .Where(s => s.Id == seriesId) .Include(s => s.ExternalSeriesMetadata) .Select(series => new PlusSeriesRequestDto() @@ -721,67 +629,70 @@ public class SeriesRepository : ISeriesRepository AltSeriesName = series.LocalizedName, AniListId = series.ExternalSeriesMetadata.AniListId != 0 ? series.ExternalSeriesMetadata.AniListId - : ScrobblingService.ExtractId(series.Metadata.WebLinks, ScrobblingService.AniListWeblinkWebsite), + : ScrobblingHelper.ExtractId(series.Metadata.WebLinks, ScrobblingHelper.AniListWeblinkWebsite), MalId = series.ExternalSeriesMetadata.MalId != 0 ? series.ExternalSeriesMetadata.MalId - : ScrobblingService.ExtractId(series.Metadata.WebLinks, ScrobblingService.MalWeblinkWebsite), + : ScrobblingHelper.ExtractId(series.Metadata.WebLinks, ScrobblingHelper.MalWeblinkWebsite), CbrId = series.ExternalSeriesMetadata.CbrId, GoogleBooksId = !string.IsNullOrEmpty(series.ExternalSeriesMetadata.GoogleBooksId) ? series.ExternalSeriesMetadata.GoogleBooksId - : ScrobblingService.ExtractId(series.Metadata.WebLinks, ScrobblingService.GoogleBooksWeblinkWebsite), - MangaDexId = ScrobblingService.ExtractId(series.Metadata.WebLinks, - ScrobblingService.MangaDexWeblinkWebsite), + : ScrobblingHelper.ExtractId(series.Metadata.WebLinks, ScrobblingHelper.GoogleBooksWeblinkWebsite), + MangaDexId = ScrobblingHelper.ExtractId(series.Metadata.WebLinks, + ScrobblingHelper.MangaDexWeblinkWebsite), VolumeCount = series.Volumes.Count, ChapterCount = series.Volumes.SelectMany(v => v.Chapters).Count(c => !c.IsSpecial), Year = series.Metadata.ReleaseYear }) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); return result; } - public async Task GetSeriesCoverImageAsync(int seriesId) + public async Task GetSeriesCoverImageAsync(int seriesId, CancellationToken ct = default) { - return await _context.Series + return await context.Series .Where(s => s.Id == seriesId) .Select(s => s.CoverImage) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } /// /// Returns a list of Series that were added, ordered by Created desc /// - /// /// Library to restrict to, if 0, will apply to all libraries + /// /// Contains pagination information /// Optional filter on query + /// /// [Obsolete("Use GetRecentlyAddedV2")] - public async Task> GetRecentlyAdded(int libraryId, int userId, UserParams userParams, FilterDto filter) + public async Task> GetRecentlyAdded(int libraryId, int userId, UserParams userParams, + FilterDto filter, CancellationToken ct = default) { - var query = await CreateFilteredSearchQueryable(userId, libraryId, filter, QueryContext.Dashboard); + var query = await CreateFilteredSearchQueryable(userId, libraryId, filter, QueryContext.Dashboard, ct); var retSeries = query .OrderByDescending(s => s.Created) - .ProjectToWithProgress(_mapper, userId) + .ProjectToWithProgress(mapper, userId) .AsSplitQuery() .AsNoTracking(); - return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize, ct); } - public async Task> GetRecentlyAddedV2(int userId, UserParams userParams, FilterV2Dto filter) + public async Task> GetRecentlyAddedV2(int userId, UserParams userParams, FilterV2Dto filter, + CancellationToken ct = default) { - var query = await CreateFilteredSearchQueryableV2(userId, filter, QueryContext.Dashboard); + var query = await CreateFilteredSearchQueryableV2(userId, filter, QueryContext.Dashboard, ct: ct); var retSeries = query .OrderByDescending(s => s.Created) - .ProjectToWithProgress(_mapper, userId) + .ProjectToWithProgress(mapper, userId) .AsSplitQuery() .AsNoTracking(); - return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize, ct); } private IList ExtractFilters(int libraryId, int userId, FilterDto filter, ref List userLibraries, @@ -850,7 +761,7 @@ public class SeriesRepository : ISeriesRepository seriesIds = new List(); if (hasProgressFilter) { - seriesIds = _context.Series + seriesIds = context.Series .Include(s => s.Progress) .Select(s => new { @@ -876,37 +787,39 @@ public class SeriesRepository : ISeriesRepository /// Library to restrict to, if 0, will apply to all libraries /// Pagination information /// Optional (default null) filter on query + /// /// - public async Task> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto? filter) + public async Task> GetOnDeck(int userId, int libraryId, UserParams userParams, + FilterDto? filter, CancellationToken ct = default) { - var settings = await _context.ServerSetting + var settings = await context.ServerSetting .Select(x => x) .AsNoTracking() - .ToListAsync(); - var serverSettings = _mapper.Map(settings); + .ToListAsync(ct); + var serverSettings = mapper.Map(settings); var cutoffProgressPoint = DateTime.Now - TimeSpan.FromDays(serverSettings.OnDeckProgressDays); var cutoffLastAddedPoint = DateTime.Now - TimeSpan.FromDays(serverSettings.OnDeckUpdateDays); - var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Dashboard) + var libraryIds = context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Dashboard) .Where(id => libraryId == 0 || id == libraryId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); // Don't allow any series the user has explicitly removed - var onDeckRemovals = _context.AppUserOnDeckRemoval + var onDeckRemovals = context.AppUserOnDeckRemoval .Where(d => d.AppUserId == userId) .Select(d => d.SeriesId) .AsEnumerable(); - var query = _context.Series + var query = context.Series .Where(s => usersSeriesIds.Contains(s.Id)) .Where(s => !onDeckRemovals.Contains(s.Id)) .Select(s => new { Series = s, - PagesRead = _context.AppUserProgresses.Where(p => p.SeriesId == s.Id && p.AppUserId == userId) + PagesRead = context.AppUserProgresses.Where(p => p.SeriesId == s.Id && p.AppUserId == userId) .Sum(s1 => s1.PagesRead), - LatestReadDate = _context.AppUserProgresses + LatestReadDate = context.AppUserProgresses .Where(p => p.SeriesId == s.Id && p.AppUserId == userId) .Max(p => p.LastModified), s.LastChapterAdded, @@ -914,24 +827,24 @@ public class SeriesRepository : ISeriesRepository .Where(s => s.PagesRead > 0 && s.PagesRead < s.Series.Pages) .Where(d => d.LatestReadDate >= cutoffProgressPoint || d.LastChapterAdded >= cutoffLastAddedPoint) - .OrderByDescending(s => s.LatestReadDate) + .OrderByDescending(s => s.LatestReadDate) .ThenByDescending(s => s.LastChapterAdded) .Select(s => s.Series) - .ProjectToWithProgress(_mapper, userId) + .ProjectToWithProgress(mapper, userId) .AsSplitQuery() .AsNoTracking(); - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize, ct); } - private async Task> CreateFilteredSearchQueryable(int userId, int libraryId, FilterDto filter, QueryContext queryContext) + private async Task> CreateFilteredSearchQueryable(int userId, int libraryId, FilterDto filter, QueryContext queryContext, CancellationToken ct = default) { // NOTE: Why do we even have libraryId when the filter has the actual libraryIds? - var userLibraries = await GetUserLibrariesForFilteredQuery(libraryId, userId, queryContext); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); - var onlyParentSeries = await _context.AppUserPreferences.Where(u => u.AppUserId == userId) + var userLibraries = await GetUserLibrariesForFilteredQuery(libraryId, userId, queryContext, ct); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); + var onlyParentSeries = await context.AppUserPreferences.Where(u => u.AppUserId == userId) .Select(u => u.CollapseSeriesRelationships) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); var formats = ExtractFilters(libraryId, userId, filter, ref userLibraries, out var allPeopleIds, out var hasPeopleFilter, out var hasGenresFilter, @@ -942,18 +855,18 @@ public class SeriesRepository : ISeriesRepository IList collectionSeries = []; if (hasCollectionTagFilter) { - collectionSeries = await _context.AppUserCollection + collectionSeries = await context.AppUserCollection .Where(uc => uc.Promoted || uc.AppUserId == userId) .Where(uc => filter.CollectionTags.Contains(uc.Id)) .SelectMany(uc => uc.Items) .RestrictAgainstAgeRestriction(userRating) .Select(s => s.Id) .Distinct() - .ToListAsync(); + .ToListAsync(ct); } - var query = _context.Series + var query = context.Series .AsNoTracking() // This new style can handle any filterComparision coming from the user .HasLanguage(hasLanguageFilter, FilterComparison.Contains, filter.Languages) @@ -977,7 +890,7 @@ public class SeriesRepository : ISeriesRepository if (filter.ReadStatus.InProgress) { query = query.HasReadingProgress(hasProgressFilter, FilterComparison.GreaterThan, - 0, userId) + 0, userId) .HasReadingProgress(hasProgressFilter, FilterComparison.LessThan, 100, userId); } else if (filter.ReadStatus.Read) @@ -993,7 +906,7 @@ public class SeriesRepository : ISeriesRepository if (userRating.AgeRating != AgeRating.NotApplicable) { - // this if statement is included in the extension + // this if statement is included in the extension query = query.RestrictAgainstAgeRestriction(userRating); } @@ -1022,16 +935,17 @@ public class SeriesRepository : ISeriesRepository return query.AsSplitQuery(); } - private async Task> CreateFilteredSearchQueryableV2(int userId, FilterV2Dto filter, QueryContext queryContext, IQueryable? query = null) + private async Task> CreateFilteredSearchQueryableV2(int userId, FilterV2Dto filter, + QueryContext queryContext, IQueryable? query = null, CancellationToken ct = default) { - var userLibraries = await GetUserLibrariesForFilteredQuery(0, userId, queryContext); - var allLibraryCount = await _context.Library.CountAsync(); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); - var onlyParentSeries = await _context.AppUserPreferences.Where(u => u.AppUserId == userId) + var userLibraries = await GetUserLibrariesForFilteredQuery(0, userId, queryContext, ct); + var allLibraryCount = await context.Library.CountAsync(ct); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); + var onlyParentSeries = await context.AppUserPreferences.Where(u => u.AppUserId == userId) .Select(u => u.CollapseSeriesRelationships) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); - query ??= _context.Series + query ??= context.Series .AsNoTracking(); // When the user has no access, just return instantly @@ -1045,7 +959,7 @@ public class SeriesRepository : ISeriesRepository query = ApplyWantToReadFilter(filter, query, userId); - query = await ApplyCollectionFilter(filter, query, userId, userRating); + query = await ApplyCollectionFilter(filter, query, userId, userRating, ct); @@ -1061,12 +975,13 @@ public class SeriesRepository : ISeriesRepository return ApplyLimit(query - .Sort(userId, filter.SortOptions) - .AsSplitQuery() + .Sort(userId, filter.SortOptions) + .AsSplitQuery() , filter.LimitTo); } - private async Task> ApplyCollectionFilter(FilterV2Dto filter, IQueryable query, int userId, AgeRestriction userRating) + private async Task> ApplyCollectionFilter(FilterV2Dto filter, IQueryable query, + int userId, AgeRestriction userRating, CancellationToken ct = default) { var collectionStmt = filter.Statements.FirstOrDefault(stmt => stmt.Field == FilterField.CollectionTags); if (collectionStmt == null) return query; @@ -1078,27 +993,27 @@ public class SeriesRepository : ISeriesRepository return query; } - var collectionSeries = await _context.AppUserCollection + var collectionSeries = await context.AppUserCollection .Where(uc => uc.Promoted || uc.AppUserId == userId) .Where(uc => value.Contains(uc.Id)) .SelectMany(uc => uc.Items) .RestrictAgainstAgeRestriction(userRating) .Select(s => s.Id) .Distinct() - .ToListAsync(); + .ToListAsync(ct); if (collectionStmt.Comparison != FilterComparison.MustContains) return query.HasCollectionTags(true, collectionStmt.Comparison, value, collectionSeries); var collectionSeriesTasks = value.Select(async collectionId => { - return await _context.AppUserCollection + return await context.AppUserCollection .Where(uc => uc.Promoted || uc.AppUserId == userId) .Where(uc => uc.Id == collectionId) .SelectMany(uc => uc.Items) .RestrictAgainstAgeRestriction(userRating) .Select(s => s.Id) - .ToListAsync(); + .ToListAsync(ct); }); var collectionSeriesLists = await Task.WhenAll(collectionSeriesTasks); @@ -1115,7 +1030,7 @@ 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) + var seriesIds = context.AppUser.Where(u => u.Id == userId) .SelectMany(u => u.WantToRead) .Select(s => s.SeriesId); @@ -1278,104 +1193,83 @@ public class SeriesRepository : ISeriesRepository return query.AsSplitQuery(); } - public async Task GetSeriesMetadata(int seriesId) + public async Task GetSeriesMetadata(int seriesId, CancellationToken ct = default) { - return await _context.SeriesMetadata + return await context.SeriesMetadata .Where(metadata => metadata.SeriesId == seriesId) .Include(m => m.Genres.OrderBy(g => g.NormalizedTitle)) .Include(m => m.Tags.OrderBy(g => g.NormalizedTitle)) .Include(m => m.People) .ThenInclude(p => p.Person) .AsNoTracking() - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .AsSplitQuery() - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } - public async Task> GetSeriesDtoForCollectionAsync(int collectionId, int userId, UserParams userParams) + public async Task> GetSeriesDtoForCollectionAsync(int collectionId, int userId, + UserParams userParams, CancellationToken ct = default) { - var userLibraries = _context.Library.GetUserLibraries(userId); + var userLibraries = context.Library.GetUserLibraries(userId); - var query = _context.AppUserCollection + var query = context.AppUserCollection .Where(s => s.Id == collectionId) .Include(c => c.Items) .SelectMany(c => c.Items.Where(s => userLibraries.Contains(s.LibraryId))) .OrderBy(s => s.LibraryId) .ThenBy(s => s.SortName.ToLower()) - .ProjectToWithProgress(_mapper, userId) + .ProjectToWithProgress(mapper, userId) .AsSplitQuery(); - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize, ct); } - public async Task> GetFilesForSeries(int seriesId) + public async Task> GetFilesForSeries(int seriesId, CancellationToken ct = default) { - return await _context.Volume + return await context.Volume .Where(v => v.SeriesId == seriesId) .Include(v => v.Chapters) .ThenInclude(c => c.Files) .SelectMany(v => v.Chapters.SelectMany(c => c.Files)) .AsSplitQuery() .AsNoTracking() - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetFilesizeForSeriesAsync(int seriesId) + public async Task> GetSeriesDtoForIdsAsync(IEnumerable seriesIds, int userId, + CancellationToken ct = default) { - return await _context.Volume - .Where(v => v.SeriesId == seriesId) - .SumAsync(v => v.Chapters.Sum(c => c.Files.Sum(f => f.Bytes))); - } - - public async Task> GetFilesizeForMultipleSeriesAsync(IList seriesIds) - { - return await seriesIds.BatchToDictionaryAsync(50, batch => - _context.Volume - .Where(v => batch.Contains(v.SeriesId)) - .GroupBy(v => v.SeriesId) - .Select(g => new - { - SeriesId = g.Key, - TotalBytes = g.SelectMany(v => v.Chapters) - .SelectMany(c => c.Files) - .Sum(f => f.Bytes) - }) - .ToDictionaryAsync(x => x.SeriesId, x => x.TotalBytes)); - } - - public async Task> GetSeriesDtoForIdsAsync(IEnumerable seriesIds, int userId) - { - var allowedLibraries = _context.Library + var allowedLibraries = context.Library .Include(l => l.AppUsers) .Where(library => library.AppUsers.Any(x => x.Id == userId)) .AsSplitQuery() .Select(l => l.Id); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); - return await _context.Series + return await context.Series .RestrictAgainstAgeRestriction(userRating) .Where(s => seriesIds.Contains(s.Id) && allowedLibraries.Contains(s.LibraryId)) .OrderBy(s => s.SortName.ToLower()) - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .AsNoTracking() .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetAllCoverImagesAsync() + public async Task> GetAllCoverImagesAsync(CancellationToken ct = default) { - return (await _context.Series + return (await context.Series .Select(s => s.CoverImage) .Where(t => !string.IsNullOrEmpty(t)) - .ToListAsync())!; + .ToListAsync(ct))!; } - public async Task> GetLockedCoverImagesAsync() + public async Task> GetLockedCoverImagesAsync(CancellationToken ct = default) { - return (await _context.Series + return (await context.Series .Where(s => s.CoverImageLocked && !string.IsNullOrEmpty(s.CoverImage)) .Select(s => s.CoverImage) - .ToListAsync())!; + .ToListAsync(ct))!; } /// @@ -1383,15 +1277,15 @@ public class SeriesRepository : ISeriesRepository /// /// Defaults to 0, library to restrict count to /// - private async Task GetSeriesCount(int libraryId = 0) + private async Task GetSeriesCount(int libraryId = 0, CancellationToken ct = default) { if (libraryId > 0) { - return await _context.Series + return await context.Series .Where(s => s.LibraryId == libraryId) - .CountAsync(); + .CountAsync(ct); } - return await _context.Series.CountAsync(); + return await context.Series.CountAsync(ct); } /// @@ -1405,11 +1299,11 @@ public class SeriesRepository : ISeriesRepository return new Tuple(totalSeries, 50); } - public async Task GetChunkInfo(int libraryId = 0) + public async Task GetChunkInfo(int libraryId = 0, CancellationToken ct = default) { var (totalSeries, chunkSize) = await GetChunkSize(libraryId); - if (totalSeries == 0) return new Chunk() + if (totalSeries == 0) return new Chunk { TotalChunks = 0, TotalSize = 0, @@ -1418,7 +1312,7 @@ public class SeriesRepository : ISeriesRepository var totalChunks = Math.Max((int) Math.Ceiling((totalSeries * 1.0) / chunkSize), 1); - return new Chunk() + return new Chunk { TotalSize = totalSeries, ChunkSize = chunkSize, @@ -1433,14 +1327,16 @@ public class SeriesRepository : ISeriesRepository /// in memory, we stop after 30 series. /// Used to ensure user has access to libraries /// Page size and offset + /// /// - public async Task> GetRecentlyUpdatedSeries(int userId, UserParams? userParams) + public async Task> GetRecentlyUpdatedSeries(int userId, UserParams? userParams, + CancellationToken ct = default) { userParams ??= UserParams.Default; - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); - var items = (await GetRecentlyAddedChaptersQuery(userId)); + var items = await GetRecentlyAddedChaptersQuery(userId, ct); if (userRating.AgeRating != AgeRating.NotApplicable) { items = items.RestrictAgainstAgeRestriction(userRating); @@ -1487,17 +1383,18 @@ public class SeriesRepository : ISeriesRepository return seriesMap.Values.ToList(); } - public async Task> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind) + public async Task> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind, + CancellationToken ct = default) { - var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); + var libraryIds = context.AppUser.GetLibraryIdsForUser(userId); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); - var usersSeriesIds = _context.Series + var usersSeriesIds = context.Series .Where(s => libraryIds.Contains(s.LibraryId)) .RestrictAgainstAgeRestriction(userRating) .Select(s => s.Id); - var targetSeries = _context.SeriesRelation + var targetSeries = context.SeriesRelation .Where(sr => sr.SeriesId == seriesId && sr.RelationKind == kind && usersSeriesIds.Contains(sr.TargetSeriesId)) .Include(sr => sr.TargetSeries) @@ -1505,34 +1402,35 @@ public class SeriesRepository : ISeriesRepository .AsNoTracking() .Select(sr => sr.TargetSeriesId); - return await _context.Series + return await context.Series .Where(s => targetSeries.Contains(s.Id)) .RestrictAgainstAgeRestriction(userRating) .AsSplitQuery() .AsNoTracking() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task> GetMoreIn(int userId, int libraryId, int genreId, UserParams userParams) + public async Task> GetMoreIn(int userId, int libraryId, int genreId, UserParams userParams, + CancellationToken ct = default) { - var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Dashboard) + var libraryIds = context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Dashboard) .Where(id => libraryId == 0 || id == libraryId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); // Because this can be called from an API, we need to provide an additional check if the genre has anything the // user with age restrictions can access - var query = _context.Series + var query = context.Series .Where(s => s.Metadata.Genres.Select(g => g.Id).Contains(genreId)) .Where(s => usersSeriesIds.Contains(s.Id)) .RestrictAgainstAgeRestriction(userRating) .AsSplitQuery() - .ProjectToWithProgress(_mapper, userId); + .ProjectToWithProgress(mapper, userId); - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize, ct); } /// @@ -1541,33 +1439,35 @@ public class SeriesRepository : ISeriesRepository /// /// /// + /// /// - public async Task> GetRediscover(int userId, int libraryId, UserParams userParams) + public async Task> GetRediscover(int userId, int libraryId, UserParams userParams, + CancellationToken ct = default) { - var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) + var libraryIds = context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) .Where(id => libraryId == 0 || id == libraryId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); - var distinctSeriesIdsWithProgress = _context.AppUserProgresses + var distinctSeriesIdsWithProgress = context.AppUserProgresses .Where(s => usersSeriesIds.Contains(s.SeriesId)) .Select(p => p.SeriesId) .Distinct(); - var query = _context.Series + var query = context.Series .Where(s => distinctSeriesIdsWithProgress.Contains(s.Id) && - _context.AppUserProgresses.Where(s1 => s1.SeriesId == s.Id && s1.AppUserId == userId) + context.AppUserProgresses.Where(s1 => s1.SeriesId == s.Id && s1.AppUserId == userId) .Sum(s1 => s1.PagesRead) >= s.Pages) .AsSplitQuery() - .ProjectTo(_mapper.ConfigurationProvider); + .ProjectTo(mapper.ConfigurationProvider); - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize, ct); } - public async Task GetSeriesForMangaFile(int mangaFileId, int userId) + public async Task GetSeriesForMangaFile(int mangaFileId, int userId, CancellationToken ct = default) { - var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId, 0, QueryContext.Search); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); + var libraryIds = context.AppUser.GetLibraryIdsForUser(userId, 0, QueryContext.Search); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); - return await _context.MangaFile + return await context.MangaFile .Where(m => m.Id == mangaFileId) .AsSplitQuery() .Select(f => f.Chapter) @@ -1575,23 +1475,23 @@ public class SeriesRepository : ISeriesRepository .Select(v => v.Series) .Where(s => libraryIds.Contains(s.LibraryId)) .RestrictAgainstAgeRestriction(userRating) - .ProjectTo(_mapper.ConfigurationProvider) - .SingleOrDefaultAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .SingleOrDefaultAsync(ct); } - public async Task GetSeriesForChapter(int chapterId, int userId) + public async Task GetSeriesForChapter(int chapterId, int userId, CancellationToken ct = default) { - var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); - return await _context.Chapter + var libraryIds = context.AppUser.GetLibraryIdsForUser(userId); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); + return await context.Chapter .Where(m => m.Id == chapterId) .AsSplitQuery() .Select(c => c.Volume) .Select(v => v.Series) .Where(s => libraryIds.Contains(s.LibraryId)) .RestrictAgainstAgeRestriction(userRating) - .ProjectTo(_mapper.ConfigurationProvider) - .SingleOrDefaultAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .SingleOrDefaultAsync(ct); } /// @@ -1599,19 +1499,22 @@ public class SeriesRepository : ISeriesRepository /// /// This will be normalized in the query and checked against FolderPath and LowestFolderPath /// Additional relationships to include with the base query + /// /// - public async Task GetSeriesByFolderPath(string folder, SeriesIncludes includes = SeriesIncludes.None) + public async Task GetSeriesByFolderPath(string folder, SeriesIncludes includes = SeriesIncludes.None, + CancellationToken ct = default) { - var normalized = Services.Tasks.Scanner.Parser.Parser.NormalizePath(folder); + var normalized = folder.NormalizePath(); if (string.IsNullOrEmpty(normalized)) return null; - return await _context.Series + return await context.Series .Where(s => (!string.IsNullOrEmpty(s.FolderPath) && s.FolderPath.Equals(normalized) || (!string.IsNullOrEmpty(s.LowestFolderPath) && s.LowestFolderPath.Equals(normalized)))) .Includes(includes) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } - public async Task GetSeriesThatContainsLowestFolderPath(string path, SeriesIncludes includes = SeriesIncludes.None) + public async Task GetSeriesThatContainsLowestFolderPath(string path, + SeriesIncludes includes = SeriesIncludes.None, CancellationToken ct = default) { // Check if the path ends with a file (has a file extension) string directoryPath; @@ -1628,30 +1531,30 @@ public class SeriesRepository : ISeriesRepository } // Normalize the directory path - var normalized = Services.Tasks.Scanner.Parser.Parser.NormalizePath(directoryPath); + var normalized = directoryPath.NormalizePath(); if (string.IsNullOrEmpty(normalized)) return null; normalized = normalized.TrimEnd('/'); - return await _context.Series + return await context.Series .Where(s => !string.IsNullOrEmpty(s.LowestFolderPath) && EF.Functions.Like(normalized, s.LowestFolderPath + "%")) .Includes(includes) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } public async Task> GetAllSeriesByNameAsync(IList normalizedNames, - int userId, SeriesIncludes includes = SeriesIncludes.None) + int userId, SeriesIncludes includes = SeriesIncludes.None, CancellationToken ct = default) { - var libraryIds = _context.Library.GetUserLibraries(userId); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); + var libraryIds = context.Library.GetUserLibraries(userId); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); - return await _context.Series + return await context.Series .Where(s => normalizedNames.Contains(s.NormalizedName) || normalizedNames.Contains(s.NormalizedLocalizedName)) .Where(s => libraryIds.Contains(s.LibraryId)) .RestrictAgainstAgeRestriction(userRating) .Includes(includes) - .ToListAsync(); + .ToListAsync(ct); } @@ -1664,13 +1567,14 @@ public class SeriesRepository : ISeriesRepository /// /// /// Defaults to true. This will query against all foreign keys (deep). If false, just the series will come back + /// /// public Task GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, - MangaFormat format, bool withFullIncludes = true) + MangaFormat format, bool withFullIncludes = true, CancellationToken ct = default) { var normalizedSeries = seriesName.ToNormalized(); var normalizedLocalized = localizedName.ToNormalized(); - var query = _context.Series + var query = context.Series .Where(s => s.LibraryId == libraryId) .Where(s => s.Format == format && format != MangaFormat.Unknown) .Where(s => @@ -1684,10 +1588,10 @@ public class SeriesRepository : ISeriesRepository ); if (!withFullIncludes) { - return query.SingleOrDefaultAsync(); + return query.SingleOrDefaultAsync(ct); } - #nullable disable +#nullable disable query = query.Include(s => s.Library) .Include(s => s.Metadata) @@ -1718,23 +1622,23 @@ public class SeriesRepository : ISeriesRepository .ThenInclude(c => c.Files) .AsSplitQuery(); - return query.SingleOrDefaultAsync(); + return query.SingleOrDefaultAsync(ct); - #nullable enable +#nullable enable } public async Task GetSeriesByAnyName(string seriesName, string localizedName, IList formats, - int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None) + int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None, CancellationToken ct = default) { - var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId); + var libraryIds = context.AppUser.GetLibraryIdsForUser(userId); var normalizedSeries = seriesName.ToNormalized(); var normalizedLocalized = localizedName.ToNormalized(); - var query = _context.Series + var query = context.Series .Where(s => libraryIds.Contains(s.LibraryId)) .Where(s => formats.Contains(s.Format)); - if (aniListId.HasValue && aniListId.Value > 0) + if (aniListId is > 0) { // If AniList ID is provided, override name checks query = query.Where(s => s.ExternalSeriesMetadata.AniListId == aniListId.Value); @@ -1753,23 +1657,23 @@ public class SeriesRepository : ISeriesRepository return await query .Includes(includes) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } public async Task GetSeriesByAnyName(IList names, IList formats, - int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None) + int userId, int? aniListId = null, SeriesIncludes includes = SeriesIncludes.None, CancellationToken ct = default) { - var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId); + var libraryIds = context.AppUser.GetLibraryIdsForUser(userId); names = names.Where(s => !string.IsNullOrEmpty(s)).Distinct().ToList(); var normalizedNames = names.Select(s => s.ToNormalized()).ToList(); - var query = _context.Series + var query = context.Series .Where(s => libraryIds.Contains(s.LibraryId)) .Where(s => formats.Contains(s.Format)); - if (aniListId.HasValue && aniListId.Value > 0) + if (aniListId is > 0) { // If AniList ID is provided, override name checks query = query.Where(s => s.ExternalSeriesMetadata.AniListId == aniListId.Value || @@ -1788,15 +1692,15 @@ public class SeriesRepository : ISeriesRepository return await query .Includes(includes) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } public async Task> GetAllSeriesByAnyName(string seriesName, string localizedName, int libraryId, - MangaFormat format) + MangaFormat format, CancellationToken ct = default) { var normalizedSeries = seriesName.ToNormalized(); var normalizedLocalized = localizedName.ToNormalized(); - return await _context.Series + return await context.Series .Where(s => s.LibraryId == libraryId) .Where(s => s.Format == format && format != MangaFormat.Unknown) .Where(s => @@ -1809,7 +1713,7 @@ public class SeriesRepository : ISeriesRepository || (s.OriginalName != null && s.OriginalName.Equals(seriesName)) ) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } @@ -1818,14 +1722,16 @@ public class SeriesRepository : ISeriesRepository /// /// /// - public async Task> RemoveSeriesNotInList(IList seenSeries, int libraryId) + /// + public async Task> RemoveSeriesNotInList(IList seenSeries, int libraryId, + CancellationToken ct = default) { if (!seenSeries.Any()) return Array.Empty(); // Get all series from DB in one go, based on libraryId - var dbSeries = await _context.Series + var dbSeries = await context.Series .Where(s => s.LibraryId == libraryId) - .ToListAsync(); + .ToListAsync(ct); // Get a set of matching series ids for the given parsedSeries var ids = new HashSet(); @@ -1850,161 +1756,165 @@ public class SeriesRepository : ISeriesRepository .ToList(); // Remove series in bulk - _context.Series.RemoveRange(seriesToRemove); + context.Series.RemoveRange(seriesToRemove); return seriesToRemove; } - public async Task> GetHighlyRated(int userId, int libraryId, UserParams userParams) + public async Task> GetHighlyRated(int userId, int libraryId, UserParams userParams, + CancellationToken ct = default) { - var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) + var libraryIds = context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) .Where(id => libraryId == 0 || id == libraryId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); - var distinctSeriesIdsWithHighRating = _context.AppUserRating + var distinctSeriesIdsWithHighRating = context.AppUserRating .Where(s => usersSeriesIds.Contains(s.SeriesId) && s.Rating > 4) .Select(p => p.SeriesId) .Distinct(); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); - var query = _context.Series + var query = context.Series .Where(s => distinctSeriesIdsWithHighRating.Contains(s.Id)) .RestrictAgainstAgeRestriction(userRating) .AsSplitQuery() - .OrderByDescending(s => _context.AppUserRating.Where(r => r.SeriesId == s.Id).Select(r => r.Rating).Average()) - .ProjectToWithProgress(_mapper, userId); + .OrderByDescending(s => context.AppUserRating.Where(r => r.SeriesId == s.Id).Select(r => r.Rating).Average()) + .ProjectToWithProgress(mapper, userId); - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize, ct); } - public async Task> GetQuickReads(int userId, int libraryId, UserParams userParams) + public async Task> GetQuickReads(int userId, int libraryId, UserParams userParams, + CancellationToken ct = default) { - var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) + var libraryIds = context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) .Where(id => libraryId == 0 || id == libraryId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); - var distinctSeriesIdsWithProgress = _context.AppUserProgresses + var distinctSeriesIdsWithProgress = context.AppUserProgresses .Where(s => usersSeriesIds.Contains(s.SeriesId)) .Select(p => p.SeriesId) .Distinct(); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); - var query = _context.Series + var query = context.Series .Where(s => ( - (s.Pages / ReaderService.AvgPagesPerMinute / 60 < 10 && s.Format != MangaFormat.Epub) - || (s.WordCount * ReaderService.AvgWordsPerHour < 10 && s.Format == MangaFormat.Epub)) - && !distinctSeriesIdsWithProgress.Contains(s.Id) && - usersSeriesIds.Contains(s.Id)) + (s.Pages / IReaderService.AvgPagesPerMinute / 60 < 10 && s.Format != MangaFormat.Epub) + || (s.WordCount * IReaderService.AvgWordsPerHour < 10 && s.Format == MangaFormat.Epub)) + && !distinctSeriesIdsWithProgress.Contains(s.Id) && + usersSeriesIds.Contains(s.Id)) .Where(s => s.Metadata.PublicationStatus != PublicationStatus.OnGoing) .RestrictAgainstAgeRestriction(userRating) .AsSplitQuery() - .ProjectTo(_mapper.ConfigurationProvider); + .ProjectTo(mapper.ConfigurationProvider); - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize, ct); } - public async Task> GetQuickCatchupReads(int userId, int libraryId, UserParams userParams) + public async Task> GetQuickCatchupReads(int userId, int libraryId, UserParams userParams, + CancellationToken ct = default) { - var libraryIds = _context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) + var libraryIds = context.AppUser.GetLibraryIdsForUser(userId, libraryId, QueryContext.Dashboard) .Where(id => libraryId == 0 || id == libraryId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); - var distinctSeriesIdsWithProgress = _context.AppUserProgresses + var distinctSeriesIdsWithProgress = context.AppUserProgresses .Where(s => usersSeriesIds.Contains(s.SeriesId)) .Select(p => p.SeriesId) .Distinct(); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); - var query = _context.Series + var query = context.Series .Where(s => ( - (s.Pages / ReaderService.AvgPagesPerMinute / 60 < 10 && s.Format != MangaFormat.Epub) - || (s.WordCount * ReaderService.AvgWordsPerHour < 10 && s.Format == MangaFormat.Epub)) + (s.Pages / IReaderService.AvgPagesPerMinute / 60 < 10 && s.Format != MangaFormat.Epub) + || (s.WordCount * IReaderService.AvgWordsPerHour < 10 && s.Format == MangaFormat.Epub)) && !distinctSeriesIdsWithProgress.Contains(s.Id) && usersSeriesIds.Contains(s.Id)) .Where(s => s.Metadata.PublicationStatus == PublicationStatus.OnGoing) .RestrictAgainstAgeRestriction(userRating) .AsSplitQuery() - .ProjectTo(_mapper.ConfigurationProvider); + .ProjectTo(mapper.ConfigurationProvider); - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize, ct); } - public async Task GetRelatedSeries(int userId, int seriesId) + public async Task GetRelatedSeries(int userId, int seriesId, CancellationToken ct = default) { - var libraryIds = _context.Library.GetUserLibraries(userId); + var libraryIds = context.Library.GetUserLibraries(userId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); return new RelatedSeriesDto() { SourceSeriesId = seriesId, - Adaptations = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Adaptation, userRating), - Characters = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Character, userRating), - Prequels = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Prequel, userRating), - Sequels = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Sequel, userRating), - Contains = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Contains, userRating), - SideStories = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.SideStory, userRating), - SpinOffs = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.SpinOff, userRating), - Others = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Other, userRating), - AlternativeSettings = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.AlternativeSetting, userRating), - AlternativeVersions = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.AlternativeVersion, userRating), - Doujinshis = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Doujinshi, userRating), - Annuals = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Annual, userRating), - Parent = await _context.SeriesRelation + Adaptations = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Adaptation, userRating, ct), + Characters = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Character, userRating, ct), + Prequels = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Prequel, userRating, ct), + Sequels = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Sequel, userRating, ct), + Contains = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Contains, userRating, ct), + SideStories = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.SideStory, userRating, ct), + SpinOffs = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.SpinOff, userRating, ct), + Others = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Other, userRating, ct), + AlternativeSettings = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.AlternativeSetting, userRating, ct), + AlternativeVersions = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.AlternativeVersion, userRating, ct), + Doujinshis = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Doujinshi, userRating, ct), + Annuals = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Annual, userRating, ct), + Parent = await context.SeriesRelation .Where(r => r.TargetSeriesId == seriesId - && usersSeriesIds.Contains(r.TargetSeriesId) - && r.RelationKind != RelationKind.Prequel - && r.RelationKind != RelationKind.Sequel - && r.RelationKind != RelationKind.Edition) - .Select(sr => sr.Series) + && usersSeriesIds.Contains(r.TargetSeriesId) + && r.RelationKind != RelationKind.Prequel + && r.RelationKind != RelationKind.Sequel + && r.RelationKind != RelationKind.Edition) + .Select(sr => sr.Series) .RestrictAgainstAgeRestriction(userRating) .AsSplitQuery() .AsNoTracking() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(), - Editions = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Edition, userRating) + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct), + Editions = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Edition, userRating, ct) }; } private IQueryable GetSeriesIdsForLibraryIds(IQueryable libraryIds) { - return _context.Series + return context.Series .Where(s => libraryIds.Contains(s.LibraryId)) .Select(s => s.Id); } - private async Task> GetRelatedSeriesQuery(int seriesId, IEnumerable usersSeriesIds, RelationKind kind, AgeRestriction userRating) + private async Task> GetRelatedSeriesQuery(int seriesId, IEnumerable usersSeriesIds, + RelationKind kind, AgeRestriction userRating, CancellationToken ct = default) { - return await _context.Series.SelectMany(s => - s.Relations.Where(sr => sr.RelationKind == kind && sr.SeriesId == seriesId && usersSeriesIds.Contains(sr.TargetSeriesId)) - .Select(sr => sr.TargetSeries)) + return await context.Series.SelectMany(s => + s.Relations.Where(sr => sr.RelationKind == kind && sr.SeriesId == seriesId && usersSeriesIds.Contains(sr.TargetSeriesId)) + .Select(sr => sr.TargetSeries)) .RestrictAgainstAgeRestriction(userRating) .AsSplitQuery() .AsNoTracking() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - private async Task> GetRecentlyAddedChaptersQuery(int userId) + private async Task> GetRecentlyAddedChaptersQuery(int userId, CancellationToken ct = default) { - var libraryIds = await _context.AppUser + var libraryIds = await context.AppUser .Where(u => u.Id == userId) .SelectMany(u => u.Libraries) .Where(l => l.IncludeInDashboard) .Select(l => l.Id) - .ToListAsync(); + .ToListAsync(ct); var withinLastWeek = DateTime.Now - TimeSpan.FromDays(12); - return _context.Chapter + return context.Chapter .Where(c => c.Created >= withinLastWeek).AsNoTracking() .Include(c => c.Volume) .ThenInclude(v => v.Series) .ThenInclude(s => s.Library) .OrderByDescending(c => c.Created) - .Select(c => new RecentlyAddedSeries() + .Select(c => new RecentlyAddedSeriesDto { LibraryId = c.Volume.Series.LibraryId, LibraryType = c.Volume.Series.Library.Type, @@ -2014,8 +1924,8 @@ public class SeriesRepository : ISeriesRepository VolumeId = c.VolumeId, ChapterId = c.Id, Format = c.Volume.Series.Format, - ChapterNumber = c.MinNumber + string.Empty, // TODO: Refactor this - ChapterRange = c.Range, // TODO: Refactor this + ChapterNumber = c.MinNumber + string.Empty, // default: Refactor this + ChapterRange = c.Range, // default: Refactor this IsSpecial = c.IsSpecial, VolumeNumber = c.Volume.MinNumber, ChapterTitle = c.Title, @@ -2027,10 +1937,11 @@ public class SeriesRepository : ISeriesRepository } [Obsolete("Use GetWantToReadForUserV2Async")] - public async Task> GetWantToReadForUserAsync(int userId, UserParams userParams, FilterDto filter) + public async Task> GetWantToReadForUserAsync(int userId, UserParams userParams, + FilterDto filter, CancellationToken ct = default) { - var libraryIds = await _context.Library.GetUserLibraries(userId).ToListAsync(); - var query = _context.AppUser + var libraryIds = await context.Library.GetUserLibraries(userId).ToListAsync(ct); + var query = context.AppUser .Where(user => user.Id == userId) .SelectMany(u => u.WantToRead) .Where(s => libraryIds.Contains(s.Series.LibraryId)) @@ -2040,182 +1951,185 @@ public class SeriesRepository : ISeriesRepository var filteredQuery = await CreateFilteredSearchQueryable(userId, 0, filter, query); - return await PagedList.CreateAsync(filteredQuery.ProjectToWithProgress(_mapper, userId), userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(filteredQuery.ProjectToWithProgress(mapper, userId), userParams.PageNumber, userParams.PageSize, ct); } - public async Task> GetWantToReadForUserV2Async(int userId, UserParams userParams, FilterV2Dto filter) + public async Task> GetWantToReadForUserV2Async(int userId, UserParams userParams, + FilterV2Dto filter, CancellationToken ct = default) { - var libraryIds = await _context.Library.GetUserLibraries(userId).ToListAsync(); - var seriesIds = await _context.AppUser + var libraryIds = await context.Library.GetUserLibraries(userId).ToListAsync(ct); + var seriesIds = await context.AppUser .Where(user => user.Id == userId) .SelectMany(u => u.WantToRead) .Where(s => libraryIds.Contains(s.Series.LibraryId)) .Select(w => w.Series.Id) .Distinct() - .ToListAsync(); + .ToListAsync(ct); - var query = await CreateFilteredSearchQueryableV2(userId, filter, QueryContext.None); + var query = await CreateFilteredSearchQueryableV2(userId, filter, QueryContext.None, ct: ct); // Apply the Want to Read filtering query = query.Where(s => seriesIds.Contains(s.Id)); var retSeries = query - .ProjectToWithProgress(_mapper, userId) + .ProjectToWithProgress(mapper, userId) .AsSplitQuery() .AsNoTracking(); - return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(retSeries, userParams.PageNumber, userParams.PageSize, ct); } - public async Task> GetWantToReadForUserAsync(int userId) + public async Task> GetWantToReadForUserAsync(int userId, CancellationToken ct = default) { - var libraryIds = await _context.Library.GetUserLibraries(userId).ToListAsync(); - return await _context.AppUser + var libraryIds = await context.Library.GetUserLibraries(userId).ToListAsync(ct); + return await context.AppUser .Where(user => user.Id == userId) .SelectMany(u => u.WantToRead) .Where(s => libraryIds.Contains(s.Series.LibraryId)) .Select(w => w.Series) .AsSplitQuery() .AsNoTracking() - .ToListAsync(); + .ToListAsync(ct); } /// /// 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 GetSeriesDtoByNamesAndMetadataIds(IEnumerable names, LibraryType libraryType, string aniListUrl, string malUrl) + public async Task GetSeriesDtoByNamesAndMetadataIds(IEnumerable names, LibraryType libraryType, + string aniListUrl, string malUrl, CancellationToken ct = default) { - var libraryIds = await _context.Library + var libraryIds = await context.Library .Where(lib => lib.Type == libraryType) .Select(l => l.Id) - .ToListAsync(); + .ToListAsync(ct); 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 + // default: I can likely work AniList and MalIds from ExternalSeriesMetadata in here + result = await context.Series .Where(s => !string.IsNullOrEmpty(s.Metadata.WebLinks)) .Where(s => libraryIds.Contains(s.Library.Id)) .WhereIf(!string.IsNullOrEmpty(aniListUrl), s => s.Metadata.WebLinks.Contains(aniListUrl)) .WhereIf(!string.IsNullOrEmpty(malUrl), s => s.Metadata.WebLinks.Contains(malUrl)) - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .AsSplitQuery() - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } if (result != null) return result; - return await _context.Series + return await context.Series .Where(s => normalizedNames.Contains(s.NormalizedName) || normalizedNames.Contains(s.NormalizedLocalizedName)) .Where(s => libraryIds.Contains(s.Library.Id)) - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .AsSplitQuery() - .FirstOrDefaultAsync(); // Some users may have improperly configured libraries + .FirstOrDefaultAsync(ct); // Some users may have improperly configured libraries } - public async Task MatchSeries(ExternalSeriesDetailDto externalSeries) + public async Task MatchSeries(ExternalSeriesDetailDto externalSeries, CancellationToken ct = default) { - var libraryIds = await _context.Library + var libraryIds = await context.Library .Where(lib => externalSeries.PlusMediaFormat.ConvertToLibraryTypes().Contains(lib.Type)) .Select(l => l.Id) - .ToListAsync(); + .ToListAsync(ct); var normalizedNames = (externalSeries.Synonyms ?? Enumerable.Empty()) .Prepend(externalSeries.Name) .Select(n => n.ToNormalized()) .ToList(); - var aniListWebLink = - ScrobblingService.CreateUrl(ScrobblingService.AniListWeblinkWebsite, externalSeries.AniListId); - var malWebLink = - ScrobblingService.CreateUrl(ScrobblingService.MalWeblinkWebsite, externalSeries.MALId); + var aniListWebLink = ScrobblingHelper.CreateUrl(ScrobblingHelper.AniListWeblinkWebsite, externalSeries.AniListId); + var malWebLink = ScrobblingHelper.CreateUrl(ScrobblingHelper.MalWeblinkWebsite, externalSeries.MALId); Series? result = null; if (!string.IsNullOrEmpty(aniListWebLink) || !string.IsNullOrEmpty(malWebLink)) { - result = await _context.Series + result = await context.Series .Where(s => !string.IsNullOrEmpty(s.Metadata.WebLinks)) .Where(s => libraryIds.Contains(s.Library.Id)) .WhereIf(!string.IsNullOrEmpty(aniListWebLink), s => s.Metadata.WebLinks.Contains(aniListWebLink)) .WhereIf(!string.IsNullOrEmpty(malWebLink), s => s.Metadata.WebLinks.Contains(malWebLink)) .Include(s => s.Metadata) .AsSplitQuery() - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } if (result != null) return result; - return await _context.Series + return await context.Series .Where(s => normalizedNames.Contains(s.NormalizedName) || normalizedNames.Contains(s.NormalizedLocalizedName)) .Where(s => libraryIds.Contains(s.Library.Id)) .AsSplitQuery() .Include(s => s.Metadata) - .FirstOrDefaultAsync(); // Some users may have improperly configured libraries + .FirstOrDefaultAsync(ct); // Some users may have improperly configured libraries } /// /// Returns the Average rating for all users within Kavita instance /// /// - public async Task GetAverageUserRating(int seriesId, int userId) + /// + /// + public async Task GetAverageUserRating(int seriesId, int userId, CancellationToken ct = default) { // If there is 0 or 1 rating and that rating is you, return 0 back - var countOfRatingsThatAreUser = await _context.AppUserRating + var countOfRatingsThatAreUser = await context.AppUserRating .Where(r => r.SeriesId == seriesId && r.HasBeenRated) - .CountAsync(u => u.AppUserId == userId); + .CountAsync(u => u.AppUserId == userId, cancellationToken: ct); if (countOfRatingsThatAreUser == 1) { return 0; } - var avg = (await _context.AppUserRating + var avg = (await context.AppUserRating .Where(r => r.SeriesId == seriesId && r.HasBeenRated) - .AverageAsync(r => (int?) r.Rating)); + .AverageAsync(r => (int?) r.Rating, cancellationToken: ct)); return avg.HasValue ? (int) (avg.Value * 20) : 0; } - public async Task RemoveFromOnDeck(int seriesId, int userId) + public async Task RemoveFromOnDeck(int seriesId, int userId, CancellationToken ct = default) { - var existingEntry = await _context.AppUserOnDeckRemoval + var existingEntry = await context.AppUserOnDeckRemoval .Where(u => u.Id == userId && u.SeriesId == seriesId) - .AnyAsync(); + .AnyAsync(ct); if (existingEntry) return; - _context.AppUserOnDeckRemoval.Add(new AppUserOnDeckRemoval() + context.AppUserOnDeckRemoval.Add(new AppUserOnDeckRemoval() { SeriesId = seriesId, AppUserId = userId }); - await _context.SaveChangesAsync(); + await context.SaveChangesAsync(ct); } - public async Task ClearOnDeckRemoval(int seriesId, int userId) + public async Task ClearOnDeckRemoval(int seriesId, int userId, CancellationToken ct = default) { - var existingEntry = await _context.AppUserOnDeckRemoval + var existingEntry = await context.AppUserOnDeckRemoval .Where(u => u.AppUserId == userId && u.SeriesId == seriesId) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); if (existingEntry == null) return; - _context.AppUserOnDeckRemoval.Remove(existingEntry); - await _context.SaveChangesAsync(); + context.AppUserOnDeckRemoval.Remove(existingEntry); + await context.SaveChangesAsync(ct); } - public async Task IsSeriesInWantToRead(int userId, int seriesId) + public async Task IsSeriesInWantToRead(int userId, int seriesId, CancellationToken ct = default) { - var libraryIds = await _context.Library.GetUserLibraries(userId).ToListAsync(); - return await _context.AppUser + var libraryIds = await context.Library.GetUserLibraries(userId).ToListAsync(ct); + return await context.AppUser .Where(user => user.Id == userId) .SelectMany(u => u.WantToRead.Where(s => s.SeriesId == seriesId && libraryIds.Contains(s.Series.LibraryId))) .AsSplitQuery() .AsNoTracking() - .AnyAsync(); + .AnyAsync(ct); } - public async Task>> GetFolderPathMap(int libraryId) + public async Task>> GetFolderPathMap(int libraryId, + CancellationToken ct = default) { - var info = await _context.Series + var info = await context.Series .Where(s => s.LibraryId == libraryId) .AsNoTracking() .Where(s => s.FolderPath != null) @@ -2228,7 +2142,7 @@ public class SeriesRepository : ISeriesRepository Format = s.Format, LibraryRoots = s.Library.Folders.Select(f => f.Path) }) - .ToListAsync(); + .ToListAsync(ct); var map = new Dictionary>(); foreach (var series in info) @@ -2268,15 +2182,16 @@ public class SeriesRepository : ISeriesRepository /// Returns the highest Age Rating for a list of Series. Defaults to /// /// + /// /// - public async Task GetMaxAgeRatingFromSeriesAsync(IEnumerable seriesIds) + public async Task GetMaxAgeRatingFromSeriesAsync(IEnumerable seriesIds, CancellationToken ct = default) { - var ret = await _context.Series + var ret = await context.Series .Where(s => seriesIds.Contains(s.Id)) .Include(s => s.Metadata) .Select(s => s.Metadata.AgeRating) .OrderBy(s => s) - .LastOrDefaultAsync(); + .LastOrDefaultAsync(ct); if (ret == null) return AgeRating.Unknown; return ret; diff --git a/Kavita.Database/Repositories/SettingsRepository.cs b/Kavita.Database/Repositories/SettingsRepository.cs new file mode 100644 index 000000000..e23484bda --- /dev/null +++ b/Kavita.Database/Repositories/SettingsRepository.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.MetadataMatching; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Repositories; + + +public class SettingsRepository(DataContext context, IMapper mapper) : ISettingsRepository +{ + public void Update(ServerSetting settings) + { + context.Entry(settings).State = EntityState.Modified; + } + + public void Update(MetadataSettings settings) + { + context.Entry(settings).State = EntityState.Modified; + } + + public void RemoveRange(List fieldMappings) + { + context.MetadataFieldMapping.RemoveRange(fieldMappings); + } + + public void Remove(ServerSetting setting) + { + context.Remove(setting); + } + + public async Task GetExternalSeriesMetadata(int seriesId, CancellationToken ct = default) + { + return await context.ExternalSeriesMetadata + .Where(s => s.SeriesId == seriesId) + .FirstOrDefaultAsync(ct); + } + + public async Task GetMetadataSettings(CancellationToken ct = default) + { + return await context.MetadataSettings + .Include(m => m.FieldMappings) + .FirstAsync(ct); + } + + public async Task GetMetadataSettingDto(CancellationToken ct = default) + { + return await context.MetadataSettings + .Include(m => m.FieldMappings) + .ProjectTo(mapper.ConfigurationProvider) + .FirstAsync(ct); + } + + public async Task GetSettingsDtoAsync(CancellationToken ct = default) + { + var settings = await context.ServerSetting + .Select(x => x) + .AsNoTracking() + .ToListAsync(ct); + return mapper.Map(settings); + } + + public Task GetSettingAsync(ServerSettingKey key, CancellationToken ct = default) + { + return context.ServerSetting.SingleOrDefaultAsync(x => x.Key == key, ct)!; + } + + public async Task> GetSettingsAsync(CancellationToken ct = default) + { + return await context.ServerSetting.ToListAsync(ct); + } +} diff --git a/Kavita.Database/Repositories/SiteThemeRepository.cs b/Kavita.Database/Repositories/SiteThemeRepository.cs new file mode 100644 index 000000000..987ee5664 --- /dev/null +++ b/Kavita.Database/Repositories/SiteThemeRepository.cs @@ -0,0 +1,91 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Models.DTOs.Theme; +using Kavita.Models.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Repositories; + +public class SiteThemeRepository(DataContext context, IMapper mapper) : ISiteThemeRepository +{ + public void Add(SiteTheme theme) + { + context.Add(theme); + } + + public void Remove(SiteTheme theme) + { + context.Remove(theme); + } + + public void Update(SiteTheme siteTheme) + { + context.Entry(siteTheme).State = EntityState.Modified; + } + + public async Task> GetThemeDtos() + { + return await context.SiteTheme + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(); + } + + public async Task GetThemeDtoByName(string themeName) + { + return await context.SiteTheme + .Where(t => t.Name.Equals(themeName)) + .ProjectTo(mapper.ConfigurationProvider) + .SingleOrDefaultAsync(); + } + + /// + /// Returns default theme, if the default theme is not available, returns the dark theme + /// + /// + public async Task GetDefaultTheme() + { + var result = await context.SiteTheme + .Where(t => t.IsDefault) + .FirstOrDefaultAsync(); + + if (result == null) + { + return await context.SiteTheme + .Where(t => t.NormalizedName == SiteTheme.DefaultTheme.NormalizedName) + .SingleAsync(); + } + + return result; + } + + public async Task> GetThemes() + { + return await context.SiteTheme + .ToListAsync(); + } + + public async Task GetTheme(int themeId) + { + return await context.SiteTheme + .Where(t => t.Id == themeId) + .FirstOrDefaultAsync(); + } + + public async Task IsThemeInUse(int themeId) + { + return await context.AppUserPreferences + .AnyAsync(p => p.Theme.Id == themeId); + } + + public async Task GetThemeDto(int themeId) + { + return await context.SiteTheme + .Where(t => t.Id == themeId) + .ProjectTo(mapper.ConfigurationProvider) + .SingleOrDefaultAsync(); + } +} diff --git a/API/Data/Repositories/TagRepository.cs b/Kavita.Database/Repositories/TagRepository.cs similarity index 55% rename from API/Data/Repositories/TagRepository.cs rename to Kavita.Database/Repositories/TagRepository.cs index 40d40a675..052b4c8bb 100644 --- a/API/Data/Repositories/TagRepository.cs +++ b/Kavita.Database/Repositories/TagRepository.cs @@ -1,79 +1,59 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.DTOs.Metadata; -using API.DTOs.Metadata.Browse; -using API.Entities; -using API.Extensions; -using API.Extensions.QueryExtensions; -using API.Helpers; -using API.Services.Tasks.Scanner.Parser; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Database.Extensions; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Metadata.Browse; +using Kavita.Models.Entities; +using Kavita.Models.Extensions; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; -#nullable enable +namespace Kavita.Database.Repositories; -public interface ITagRepository +public class TagRepository(DataContext context, IMapper mapper) : ITagRepository { - void Attach(Tag tag); - void Remove(Tag tag); - Task> GetAllTagsAsync(); - Task> GetAllTagsByNameAsync(IEnumerable normalizedNames); - Task> GetAllTagDtosAsync(int userId); - Task RemoveAllTagNoLongerAssociated(); - Task> GetAllTagDtosForLibrariesAsync(int userId, IList? libraryIds = null); - Task> GetAllTagsNotInListAsync(ICollection tags); - Task> GetBrowseableTag(int userId, UserParams userParams); -} - -public class TagRepository : ITagRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public TagRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - public void Attach(Tag tag) { - _context.Tag.Attach(tag); + context.Tag.Attach(tag); } public void Remove(Tag tag) { - _context.Tag.Remove(tag); + context.Tag.Remove(tag); } - public async Task RemoveAllTagNoLongerAssociated() + public async Task RemoveAllTagNoLongerAssociated(CancellationToken ct = default) { - var tagsWithNoConnections = await _context.Tag + var tagsWithNoConnections = await context.Tag .Include(p => p.SeriesMetadatas) .Include(p => p.Chapters) .Where(p => p.SeriesMetadatas.Count == 0 && p.Chapters.Count == 0) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); - _context.Tag.RemoveRange(tagsWithNoConnections); + context.Tag.RemoveRange(tagsWithNoConnections); - await _context.SaveChangesAsync(); + await context.SaveChangesAsync(ct); } - public async Task> GetAllTagDtosForLibrariesAsync(int userId, IList? libraryIds = null) + public async Task> GetAllTagDtosForLibrariesAsync(int userId, IList? libraryIds = null, + CancellationToken ct = default) { - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); - var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); + var userLibs = await context.Library.GetUserLibraries(userId).ToListAsync(ct); if (libraryIds is {Count: > 0}) { userLibs = userLibs.Where(libraryIds.Contains).ToList(); } - return await _context.Series + return await context.Series .Where(s => userLibs.Contains(s.LibraryId)) .RestrictAgainstAgeRestriction(userRating) .SelectMany(s => s.Metadata.Tags) @@ -81,24 +61,24 @@ public class TagRepository : ITagRepository .Distinct() .OrderBy(t => t.NormalizedTitle) .AsNoTracking() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task> GetAllTagsNotInListAsync(ICollection tags) + public async Task> GetAllTagsNotInListAsync(ICollection tags, CancellationToken ct = default) { // Create a dictionary mapping normalized names to non-normalized names var normalizedToOriginalMap = tags.Distinct() - .GroupBy(Parser.Normalize) + .GroupBy(t => t.ToNormalized()) .ToDictionary(group => group.Key, group => group.First()); var normalizedTagNames = normalizedToOriginalMap.Keys.ToList(); // Query the database for existing genres using the normalized names - var existingTags = await _context.Tag + var existingTags = await context.Tag .Where(g => normalizedTagNames.Contains(g.NormalizedTitle)) // Assuming you have a normalized field .Select(g => g.NormalizedTitle) - .ToListAsync(); + .ToListAsync(ct); // Find the normalized genres that do not exist in the database var missingTags = normalizedTagNames.Except(existingTags).ToList(); @@ -107,16 +87,17 @@ public class TagRepository : ITagRepository return missingTags.Select(normalizedName => normalizedToOriginalMap[normalizedName]).ToList(); } - public async Task> GetBrowseableTag(int userId, UserParams userParams) + public async Task> GetBrowseableTag(int userId, UserParams userParams, + CancellationToken ct = default) { - var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); + var ageRating = await context.AppUser.GetUserAgeRestriction(userId); - var allLibrariesCount = await _context.Library.CountAsync(); - var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); + var allLibrariesCount = await context.Library.CountAsync(ct); + var userLibs = await context.Library.GetUserLibraries(userId).ToListAsync(ct); - var seriesIds = _context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id); + var seriesIds = context.Series.Where(s => userLibs.Contains(s.LibraryId)).Select(s => s.Id); - var query = _context.Tag + var query = context.Tag .RestrictAgainstAgeRestriction(ageRating) .WhereIf(userLibs.Count != allLibrariesCount, tag => tag.Chapters.Any(cp => seriesIds.Contains(cp.Volume.SeriesId)) || @@ -138,29 +119,29 @@ public class TagRepository : ITagRepository }) .OrderBy(g => g.Title); - return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); + return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize, ct); } - public async Task> GetAllTagsAsync() + public async Task> GetAllTagsAsync(CancellationToken ct = default) { - return await _context.Tag.ToListAsync(); + return await context.Tag.ToListAsync(ct); } - public async Task> GetAllTagsByNameAsync(IEnumerable normalizedNames) + public async Task> GetAllTagsByNameAsync(IEnumerable normalizedNames, CancellationToken ct = default) { - return await _context.Tag + return await context.Tag .Where(t => normalizedNames.Contains(t.NormalizedTitle)) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetAllTagDtosAsync(int userId) + public async Task> GetAllTagDtosAsync(int userId, CancellationToken ct = default) { - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); - return await _context.Tag + var userRating = await context.AppUser.GetUserAgeRestriction(userId); + return await context.Tag .AsNoTracking() .RestrictAgainstAgeRestriction(userRating) .OrderBy(t => t.NormalizedTitle) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } } diff --git a/API/Data/Repositories/UserRepository.cs b/Kavita.Database/Repositories/UserRepository.cs similarity index 57% rename from API/Data/Repositories/UserRepository.cs rename to Kavita.Database/Repositories/UserRepository.cs index a78684055..26d96011f 100644 --- a/API/Data/Repositories/UserRepository.cs +++ b/Kavita.Database/Repositories/UserRepository.cs @@ -1,234 +1,110 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.Constants; -using API.DTOs; -using API.DTOs.Account; -using API.DTOs.Dashboard; -using API.DTOs.Filtering.v2; -using API.DTOs.KavitaPlus.Account; -using API.DTOs.Reader; -using API.DTOs.Scrobbling; -using API.DTOs.SeriesDetail; -using API.DTOs.SideNav; -using API.Entities; -using API.Entities.Enums.UserPreferences; -using API.Entities.User; -using API.Extensions; -using API.Extensions.QueryExtensions; -using API.Extensions.QueryExtensions.Filtering; -using API.Helpers; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Database.Extensions; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Account; +using Kavita.Models.DTOs.Dashboard; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.DTOs.KavitaPlus.Account; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.DTOs.SideNav; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.User; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; -#nullable enable +namespace Kavita.Database.Repositories; -[Flags] -public enum AppUserIncludes +public class UserRepository(DataContext context, UserManager userManager, IMapper mapper) + : IUserRepository { - None = 1, - Progress = 1 << 1, - Bookmarks = 1 << 2, - ReadingLists = 1 << 3, - Ratings = 1 << 4, - UserPreferences = 1 << 5, - WantToRead = 1 << 6, - ReadingListsWithItems = 1 << 7, - Devices = 1 << 8, - ScrobbleHolds = 1 << 9, - SmartFilters = 1 << 10, - DashboardStreams = 1 << 11, - SideNavStreams = 1 << 12, - ExternalSources = 1 << 13, - Collections = 1 << 14, - ChapterRatings = 1 << 15, - AuthKeys = 1 << 16 -} - -public interface IUserRepository -{ - void Add(AppUserAuthKey key); - void Add(AppUserBookmark bookmark); - void Add(AppUser bookmark); - void Update(AppUser user); - void Update(AppUserPreferences preferences); - void Update(AppUserBookmark bookmark); - void Update(AppUserDashboardStream stream); - void Update(AppUserSideNavStream stream); - void Delete(AppUser? user); - void Delete(AppUserAuthKey? key); - void Delete(AppUserBookmark bookmark); - void Delete(IEnumerable streams); - void Delete(AppUserDashboardStream stream); - void Delete(IEnumerable streams); - void Delete(AppUserSideNavStream stream); - Task> GetEmailConfirmedMemberDtosAsync(bool emailConfirmed = true); - Task> GetAdminUsersAsync(); - Task IsUserAdminAsync(AppUser? user); - Task> GetRoles(int userId); - Task> GetRolesByAuthKey(string? apiKey); - Task GetUserRatingAsync(int seriesId, int userId); - Task GetUserChapterRatingAsync(int userId, int chapterId); - Task> GetUserRatingDtosForSeriesAsync(int seriesId, int userId); - Task> GetUserRatingDtosForChapterAsync(int chapterId, int userId); - Task GetPreferencesAsync(string username); - Task> GetBookmarkDtosForSeries(int userId, int seriesId); - Task> GetBookmarkDtosForVolume(int userId, int volumeId); - Task> GetBookmarkDtosForChapter(int userId, int chapterId); - Task> GetAllBookmarkDtos(int userId, FilterV2Dto filter); - Task> GetAllBookmarksAsync(); - Task GetBookmarkForPage(int page, int chapterId, int imageOffset, int userId); - Task GetBookmarkAsync(int bookmarkId); - Task GetUserDtoByAuthKeyAsync(string authKey); - Task GetUserIdByAuthKeyAsync(string authKey); - Task GetUserDtoById(int userId); - Task GetUserByUsernameAsync(string username, AppUserIncludes includeFlags = AppUserIncludes.None); - Task GetUserByIdAsync(int userId, AppUserIncludes includeFlags = AppUserIncludes.None); - Task GetUserByAuthKey(string authKey, AppUserIncludes includeFlags = AppUserIncludes.None); - Task GetUserIdByUsernameAsync(string username); - Task> GetAllBookmarksByIds(IList bookmarkIds); - Task GetUserByEmailAsync(string email, AppUserIncludes includes = AppUserIncludes.None); - Task> GetAllPreferencesByThemeAsync(int themeId); - Task> GetAllPreferencesByFontAsync(string fontName); - Task HasAccessToLibrary(int libraryId, int userId); - Task HasAccessToSeries(int userId, int seriesId); - Task> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None, bool track = true); - Task GetUserByConfirmationToken(string token); - Task GetDefaultAdminUser(AppUserIncludes includes = AppUserIncludes.None); - Task> GetSeriesWithRatings(int userId); - Task> GetSeriesWithReviews(int userId); - Task HasHoldOnSeries(int userId, int seriesId); - Task> GetHolds(int userId); - Task GetLocale(int userId); - Task> GetDashboardStreams(int userId, bool visibleOnly = false); - Task> GetAllDashboardStreams(); - Task GetDashboardStream(int streamId); - Task> GetDashboardStreamWithFilter(int filterId); - Task> GetSideNavStreams(int userId, bool visibleOnly = false); - Task GetSideNavStream(int streamId); - Task GetSideNavStreamWithUser(int streamId); - Task> GetSideNavStreamWithFilter(int filterId); - Task> GetSideNavStreamsByLibraryId(int libraryId); - Task> GetSideNavStreamWithExternalSource(int externalSourceId); - Task> GetDashboardStreamsByIds(IList streamIds); - Task> GetUserTokenInfo(); - Task GetUserByDeviceEmail(string deviceEmail); - Task> GetAnnotations(int userId, int chapterId); - Task> GetAnnotationsByPage(int userId, int chapterId, int pageNum); - /// - /// Try getting a user by the id provided by OIDC - /// - /// - /// - /// - Task GetByOidcId(string? oidcId, AppUserIncludes includes = AppUserIncludes.None); - Task GetAnnotationDtoById(int userId, int annotationId); - Task> GetAnnotationDtosBySeries(int userId, int seriesId); - Task UpdateUserAsActive(int userId); - Task> GetAllReviewsForUser(int userId, int requestingUserId, string? query = null, float? ratingFilter = null); - Task GetCoverImageAsync(int userId, int requestingUserId); - Task GetPersonCoverImageAsync(int personId); - Task> GetAuthKeysForUserId(int userId); - Task> GetAllAuthKeysDtosWithExpiration(); - Task GetAuthKeyById(int authKeyId); - Task GetAuthKeyExpiration(string authKey, int userId); - Task GetSocialPreferencesForUser(int userId); - Task GetPreferencesForUser(int userId); - Task GetOpdsPreferences(int userId); -} - -public class UserRepository : IUserRepository -{ - private readonly DataContext _context; - private readonly UserManager _userManager; - private readonly IMapper _mapper; - - public UserRepository(DataContext context, UserManager userManager, IMapper mapper) - { - _context = context; - _userManager = userManager; - _mapper = mapper; - } - public void Add(AppUserAuthKey key) { - _context.AppUserAuthKey.Add(key); + context.AppUserAuthKey.Add(key); } public void Add(AppUserBookmark bookmark) { - _context.AppUserBookmark.Add(bookmark); + context.AppUserBookmark.Add(bookmark); } public void Add(AppUser user) { - _context.AppUser.Add(user); + context.AppUser.Add(user); } public void Update(AppUser user) { - _context.Entry(user).State = EntityState.Modified; + context.Entry(user).State = EntityState.Modified; } public void Update(AppUserPreferences preferences) { - _context.Entry(preferences).State = EntityState.Modified; + context.Entry(preferences).State = EntityState.Modified; } public void Update(AppUserBookmark bookmark) { - _context.Entry(bookmark).State = EntityState.Modified; + context.Entry(bookmark).State = EntityState.Modified; } public void Update(AppUserDashboardStream stream) { - _context.Entry(stream).State = EntityState.Modified; + context.Entry(stream).State = EntityState.Modified; } public void Update(AppUserSideNavStream stream) { - _context.Entry(stream).State = EntityState.Modified; + context.Entry(stream).State = EntityState.Modified; } public void Delete(AppUser? user) { if (user == null) return; - _context.AppUser.Remove(user); + context.AppUser.Remove(user); } public void Delete(AppUserAuthKey? key) { if (key == null) return; - _context.AppUserAuthKey.Remove(key); + context.AppUserAuthKey.Remove(key); } public void Delete(AppUserBookmark bookmark) { - _context.AppUserBookmark.Remove(bookmark); + context.AppUserBookmark.Remove(bookmark); } public void Delete(IEnumerable streams) { - _context.AppUserDashboardStream.RemoveRange(streams); + context.AppUserDashboardStream.RemoveRange(streams); } public void Delete(AppUserDashboardStream stream) { - _context.AppUserDashboardStream.Remove(stream); + context.AppUserDashboardStream.Remove(stream); } public void Delete(IEnumerable streams) { - _context.AppUserSideNavStream.RemoveRange(streams); + context.AppUserSideNavStream.RemoveRange(streams); } public void Delete(AppUserSideNavStream stream) { - _context.AppUserSideNavStream.Remove(stream); + context.AppUserSideNavStream.Remove(stream); } @@ -237,13 +113,14 @@ public class UserRepository : IUserRepository /// /// /// Includes() you want. Pass multiple with flag1 | flag2 + /// /// - public async Task GetUserByUsernameAsync(string username, AppUserIncludes includeFlags = AppUserIncludes.None) + public async Task GetUserByUsernameAsync(string username, AppUserIncludes includeFlags = AppUserIncludes.None, CancellationToken ct = default) { - return await _context.Users + return await context.Users .Where(x => x.UserName == username) .Includes(includeFlags) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } /// @@ -251,44 +128,45 @@ public class UserRepository : IUserRepository /// /// /// Includes() you want. Pass multiple with flag1 | flag2 + /// /// - public async Task GetUserByIdAsync(int userId, AppUserIncludes includeFlags = AppUserIncludes.None) + public async Task GetUserByIdAsync(int userId, AppUserIncludes includeFlags = AppUserIncludes.None, CancellationToken ct = default) { - return await _context.Users + return await context.Users .Where(x => x.Id == userId) .Includes(includeFlags) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task GetUserByAuthKey(string authKey, AppUserIncludes includeFlags = AppUserIncludes.None) + public async Task GetUserByAuthKey(string authKey, AppUserIncludes includeFlags = AppUserIncludes.None, CancellationToken ct = default) { if (string.IsNullOrEmpty(authKey)) return null; - return await _context.AppUserAuthKey + return await context.AppUserAuthKey .Where(ak => ak.Key == authKey) .HasNotExpired() .Select(ak => ak.AppUser) .Includes(includeFlags) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task> GetAllBookmarksAsync() + public async Task> GetAllBookmarksAsync(CancellationToken ct = default) { - return await _context.AppUserBookmark.ToListAsync(); + return await context.AppUserBookmark.ToListAsync(ct); } - public async Task GetBookmarkForPage(int page, int chapterId, int imageOffset, int userId) + public async Task GetBookmarkForPage(int page, int chapterId, int imageOffset, int userId, CancellationToken ct = default) { - return await _context.AppUserBookmark + return await context.AppUserBookmark .Where(b => b.Page == page && b.ChapterId == chapterId && b.AppUserId == userId && b.ImageOffset == imageOffset) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task GetBookmarkAsync(int bookmarkId) + public async Task GetBookmarkAsync(int bookmarkId, CancellationToken ct = default) { - return await _context.AppUserBookmark + return await context.AppUserBookmark .Where(b => b.Id == bookmarkId) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } @@ -296,13 +174,14 @@ public class UserRepository : IUserRepository /// This fetches the Id for a user. Use whenever you just need an ID. /// /// + /// /// - public async Task GetUserIdByUsernameAsync(string username) + public async Task GetUserIdByUsernameAsync(string username, CancellationToken ct = default) { - return await _context.Users + return await context.Users .Where(x => x.UserName == username) .Select(u => u.Id) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } @@ -310,140 +189,183 @@ public class UserRepository : IUserRepository /// Returns all Bookmarks for a given set of Ids /// /// + /// /// - public async Task> GetAllBookmarksByIds(IList bookmarkIds) + public async Task> GetAllBookmarksByIds(IList bookmarkIds, CancellationToken ct = default) { - return await _context.AppUserBookmark + return await context.AppUserBookmark .Where(b => bookmarkIds.Contains(b.Id)) .OrderBy(b => b.Created) - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetUserByEmailAsync(string email, AppUserIncludes includes = AppUserIncludes.None) + public async Task GetUserByEmailAsync(string email, AppUserIncludes includes = AppUserIncludes.None, CancellationToken ct = default) { var lowerEmail = email.ToLower(); - return await _context.AppUser + return await context.AppUser .Includes(includes) - .FirstOrDefaultAsync(u => u.Email != null && u.Email.ToLower().Equals(lowerEmail)); + .FirstOrDefaultAsync(u => u.Email != null && u.Email.ToLower().Equals(lowerEmail), ct); } - public async Task> GetAllPreferencesByThemeAsync(int themeId) + public async Task> GetAllPreferencesByThemeAsync(int themeId, CancellationToken ct = default) { - return await _context.AppUserPreferences + return await context.AppUserPreferences .Include(p => p.Theme) .Where(p => p.Theme.Id == themeId) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetAllPreferencesByFontAsync(string fontName) + public async Task> GetAllPreferencesByFontAsync(string fontName, CancellationToken ct = default) { - return await _context.AppUserPreferences + return await context.AppUserPreferences .Where(p => p.BookReaderFontFamily == fontName) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } - public async Task HasAccessToLibrary(int libraryId, int userId) + public async Task HasAccessToLibrary(int libraryId, int userId, CancellationToken ct = default) { - return await _context.Library + return await context.Library .Include(l => l.AppUsers) .AsSplitQuery() - .AnyAsync(library => library.AppUsers.Any(user => user.Id == userId) && library.Id == libraryId); + .AnyAsync(library => library.AppUsers.Any(user => user.Id == userId) && library.Id == libraryId, ct); } /// /// Does the user have library and age restriction access to a given series /// /// - public async Task HasAccessToSeries(int userId, int seriesId) + public async Task HasAccessToSeries(int userId, int seriesId, CancellationToken ct = default) { - var userRating = await _context.AppUser.GetUserAgeRestriction(userId); - return await _context.Series + var userRating = await context.AppUser.GetUserAgeRestriction(userId); + return await context.Series .Include(s => s.Library) .Where(s => s.Library.AppUsers.Any(user => user.Id == userId)) .RestrictAgainstAgeRestriction(userRating) .AsSplitQuery() - .AnyAsync(s => s.Id == seriesId); + .AnyAsync(s => s.Id == seriesId, ct); } - public async Task> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None, bool track = true) + public async Task HasAccessToVolume(int userId, int volumeId, CancellationToken ct = default) { - var query = _context.AppUser.Includes(includeFlags); + var userRating = await context.AppUser.GetUserAgeRestriction(userId); + return await context.Volume + .Where(v => v.Id == volumeId) + .Include(v => v.Series) + .ThenInclude(s => s.Library) + .Where(v => v.Series.Library.AppUsers.Any(user => user.Id == userId)) + .Select(v => v.Series) + .RestrictAgainstAgeRestriction(userRating) + .AsSplitQuery() + .AnyAsync(ct); + } + + public async Task HasAccessToChapter(int userId, int chapterId, CancellationToken ct = default) + { + var userRating = await context.AppUser.GetUserAgeRestriction(userId); + return await context.Chapter + .Include(c => c.Volume) + .ThenInclude(v => v.Series) + .ThenInclude(s => s.Library) + .Where(c => c.Volume.Series.Library.AppUsers.Any(user => user.Id == userId)) + .RestrictAgainstAgeRestriction(userRating) + .AsSplitQuery() + .AnyAsync(c => c.Id == chapterId, ct); + } + + public async Task HasAccessToPerson(int userId, int personId, CancellationToken ct = default) + { + var userRating = await context.AppUser.GetUserAgeRestriction(userId); + return await context.Person + .RestrictAgainstAgeRestriction(userRating) + .AnyAsync(p => p.Id == personId, ct); + } + + public Task HasAccessToReadingList(int userId, int readingListId, CancellationToken ct = default) + { + return context.ReadingList + .Where(rl => rl.AppUserId == userId || rl.Promoted) + .AnyAsync(rl => rl.Id == readingListId, ct); + } + + public async Task> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None, bool track = true, CancellationToken ct = default) + { + var query = context.AppUser.Includes(includeFlags); if (track) { - return await query.ToListAsync(); + return await query.ToListAsync(ct); } return await query .AsNoTracking() - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetUserByConfirmationToken(string token) + public async Task GetUserByConfirmationToken(string token, CancellationToken ct = default) { - return await _context.AppUser - .SingleOrDefaultAsync(u => u.ConfirmationToken != null && u.ConfirmationToken.Equals(token)); + return await context.AppUser + .SingleOrDefaultAsync(u => u.ConfirmationToken != null && u.ConfirmationToken.Equals(token), ct); } /// /// Returns the first admin account created /// /// - public async Task GetDefaultAdminUser(AppUserIncludes includes = AppUserIncludes.None) + public async Task GetDefaultAdminUser(AppUserIncludes includes = AppUserIncludes.None, CancellationToken ct = default) { - return await _context.AppUser + return await context.AppUser .Includes(includes) .Where(u => u.UserRoles.Any(r => r.Role.Name == PolicyConstants.AdminRole)) .OrderBy(u => u.Created) - .FirstAsync(); + .FirstAsync(ct); } - public async Task> GetSeriesWithRatings(int userId) + public async Task> GetSeriesWithRatings(int userId, CancellationToken ct = default) { - return await _context.AppUserRating + return await context.AppUserRating .Where(u => u.AppUserId == userId && u.Rating > 0) .Include(u => u.Series) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetSeriesWithReviews(int userId) + public async Task> GetSeriesWithReviews(int userId, CancellationToken ct = default) { - return await _context.AppUserRating + return await context.AppUserRating .Where(u => u.AppUserId == userId && !string.IsNullOrEmpty(u.Review)) .Include(u => u.Series) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } - public async Task HasHoldOnSeries(int userId, int seriesId) + public async Task HasHoldOnSeries(int userId, int seriesId, CancellationToken ct = default) { - return await _context.AppUser + return await context.AppUser .AsSplitQuery() - .AnyAsync(u => u.ScrobbleHolds.Select(s => s.SeriesId).Contains(seriesId) && u.Id == userId); + .AnyAsync(u => u.ScrobbleHolds.Select(s => s.SeriesId).Contains(seriesId) && u.Id == userId, ct); } - public async Task> GetHolds(int userId) + public async Task> GetHolds(int userId, CancellationToken ct = default) { - return await _context.ScrobbleHold + return await context.ScrobbleHold .Where(s => s.AppUserId == userId) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task GetLocale(int userId) + public async Task GetLocale(int userId, CancellationToken ct = default) { - return await _context.AppUserPreferences.Where(p => p.AppUserId == userId) + return await context.AppUserPreferences.Where(p => p.AppUserId == userId) .Select(p => p.Locale) - .SingleAsync(); + .SingleAsync(ct); } - public async Task> GetDashboardStreams(int userId, bool visibleOnly = false) + public async Task> GetDashboardStreams(int userId, bool visibleOnly = false, CancellationToken ct = default) { - return await _context.AppUserDashboardStream + return await context.AppUserDashboardStream .Where(d => d.AppUserId == userId) .WhereIf(visibleOnly, d => d.Visible) .OrderBy(d => d.Order) @@ -459,36 +381,36 @@ public class UserRepository : IUserRepository Order = d.Order, Visible = d.Visible }) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetAllDashboardStreams() + public async Task> GetAllDashboardStreams(CancellationToken ct = default) { - return await _context.AppUserDashboardStream + return await context.AppUserDashboardStream .OrderBy(d => d.Order) - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetDashboardStream(int streamId) + public async Task GetDashboardStream(int streamId, CancellationToken ct = default) { - return await _context.AppUserDashboardStream + return await context.AppUserDashboardStream .Include(d => d.SmartFilter) - .FirstOrDefaultAsync(d => d.Id == streamId); + .FirstOrDefaultAsync(d => d.Id == streamId, ct); } - public async Task> GetDashboardStreamWithFilter(int filterId) + public async Task> GetDashboardStreamWithFilter(int filterId, CancellationToken ct = default) { - return await _context.AppUserDashboardStream + return await context.AppUserDashboardStream .Include(d => d.SmartFilter) .Where(d => d.SmartFilter != null && d.SmartFilter.Id == filterId) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetSideNavStreams(int userId, bool visibleOnly = false) + public async Task> GetSideNavStreams(int userId, bool visibleOnly = false, CancellationToken ct = default) { - var sideNavStreams = await _context.AppUserSideNavStream + var sideNavStreams = await context.AppUserSideNavStream .Where(d => d.AppUserId == userId) .WhereIf(visibleOnly, d => d.Visible) .OrderBy(d => d.Order) @@ -507,16 +429,16 @@ public class UserRepository : IUserRepository Visible = d.Visible }) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); var libraryIds = sideNavStreams.Where(d => d.StreamType == SideNavStreamType.Library) .Select(d => d.LibraryId) .ToList(); - var libraryDtos = await _context.Library + var libraryDtos = await context.Library .Where(l => libraryIds.Contains(l.Id)) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); foreach (var dto in sideNavStreams.Where(dto => dto.StreamType == SideNavStreamType.Library)) { @@ -527,9 +449,9 @@ public class UserRepository : IUserRepository .Select(d => d.ExternalSourceId) .ToList(); - var externalSourceDtos = _context.AppUserExternalSource + var externalSourceDtos = context.AppUserExternalSource .Where(l => externalSourceIds.Contains(l.Id)) - .ProjectTo(_mapper.ConfigurationProvider) + .ProjectTo(mapper.ConfigurationProvider) .ToList(); foreach (var dto in sideNavStreams.Where(dto => dto.StreamType == SideNavStreamType.ExternalSource)) @@ -540,53 +462,53 @@ public class UserRepository : IUserRepository return sideNavStreams; } - public async Task GetSideNavStream(int streamId) + public async Task GetSideNavStream(int streamId, CancellationToken ct = default) { - return await _context.AppUserSideNavStream + return await context.AppUserSideNavStream .Include(d => d.SmartFilter) - .FirstOrDefaultAsync(d => d.Id == streamId); + .FirstOrDefaultAsync(d => d.Id == streamId, ct); } - public async Task GetSideNavStreamWithUser(int streamId) + public async Task GetSideNavStreamWithUser(int streamId, CancellationToken ct = default) { - return await _context.AppUserSideNavStream + return await context.AppUserSideNavStream .Include(d => d.SmartFilter) .Include(d => d.AppUser) - .FirstOrDefaultAsync(d => d.Id == streamId); + .FirstOrDefaultAsync(d => d.Id == streamId, ct); } - public async Task> GetSideNavStreamWithFilter(int filterId) + public async Task> GetSideNavStreamWithFilter(int filterId, CancellationToken ct = default) { - return await _context.AppUserSideNavStream + return await context.AppUserSideNavStream .Include(d => d.SmartFilter) .Where(d => d.SmartFilter != null && d.SmartFilter.Id == filterId) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetSideNavStreamsByLibraryId(int libraryId) + public async Task> GetSideNavStreamsByLibraryId(int libraryId, CancellationToken ct = default) { - return await _context.AppUserSideNavStream + return await context.AppUserSideNavStream .Where(d => d.LibraryId == libraryId) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetSideNavStreamWithExternalSource(int externalSourceId) + public async Task> GetSideNavStreamWithExternalSource(int externalSourceId, CancellationToken ct = default) { - return await _context.AppUserSideNavStream + return await context.AppUserSideNavStream .Where(d => d.ExternalSourceId == externalSourceId) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetDashboardStreamsByIds(IList streamIds) + public async Task> GetDashboardStreamsByIds(IList streamIds, CancellationToken ct = default) { - return await _context.AppUserSideNavStream + return await context.AppUserSideNavStream .Where(d => streamIds.Contains(d.Id)) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetUserTokenInfo() + public async Task> GetUserTokenInfo(CancellationToken ct = default) { - var users = await _context.AppUser + var users = await context.AppUser .Select(u => new { u.Id, @@ -594,7 +516,7 @@ public class UserRepository : IUserRepository u.AniListAccessToken, // JWT Token u.MalAccessToken // JWT Token }) - .ToListAsync(); + .ToListAsync(ct); var userTokenInfos = users.Select(user => new UserTokenInfo { @@ -613,12 +535,13 @@ public class UserRepository : IUserRepository /// Returns the first user with a device email matching ///
/// + /// /// - public async Task GetUserByDeviceEmail(string deviceEmail) + public async Task GetUserByDeviceEmail(string deviceEmail, CancellationToken ct = default) { - return await _context.AppUser + return await context.AppUser .Where(u => u.Devices.Any(d => d.EmailAddress == deviceEmail)) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } /// @@ -626,70 +549,71 @@ public class UserRepository : IUserRepository /// /// /// + /// /// - public async Task> GetAnnotations(int userId, int chapterId) + public async Task> GetAnnotations(int userId, int chapterId, CancellationToken ct = default) { - var userPreferences = await _context.AppUserPreferences.ToListAsync(); + var userPreferences = await context.AppUserPreferences.ToListAsync(ct); - return await _context.AppUserAnnotation + return await context.AppUserAnnotation .Where(a => a.ChapterId == chapterId) .RestrictBySocialPreferences(userId, userPreferences) .OrderBy(a => a.PageNumber) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task> GetAnnotationsByPage(int userId, int chapterId, int pageNum) + public async Task> GetAnnotationsByPage(int userId, int chapterId, int pageNum, CancellationToken ct = default) { - var userPreferences = await _context.AppUserPreferences.ToListAsync(); + var userPreferences = await context.AppUserPreferences.ToListAsync(ct); - return await _context.AppUserAnnotation + return await context.AppUserAnnotation .Where(a => a.ChapterId == chapterId && a.PageNumber == pageNum) .RestrictBySocialPreferences(userId, userPreferences) .OrderBy(a => a.PageNumber) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task GetByOidcId(string? oidcId, AppUserIncludes includes = AppUserIncludes.None) + public async Task GetByOidcId(string? oidcId, AppUserIncludes includes = AppUserIncludes.None, CancellationToken ct = default) { if (string.IsNullOrEmpty(oidcId)) return null; - return await _context.AppUser + return await context.AppUser .Where(u => u.OidcId == oidcId) .Includes(includes) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task GetAnnotationDtoById(int userId, int annotationId) + public async Task GetAnnotationDtoById(int userId, int annotationId, CancellationToken ct = default) { - var userPreferences = await _context.AppUserPreferences.ToListAsync(); + var userPreferences = await context.AppUserPreferences.ToListAsync(ct); - return await _context.AppUserAnnotation + return await context.AppUserAnnotation .Where(a => a.Id == annotationId) .RestrictBySocialPreferences(userId, userPreferences) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(ct); } - public async Task> GetAnnotationDtosBySeries(int userId, int seriesId) + public async Task> GetAnnotationDtosBySeries(int userId, int seriesId, CancellationToken ct = default) { - var userPreferences = await _context.AppUserPreferences.ToListAsync(); + var userPreferences = await context.AppUserPreferences.ToListAsync(ct); - return await _context.AppUserAnnotation + return await context.AppUserAnnotation .Where(a => a.SeriesId == seriesId) .RestrictBySocialPreferences(userId, userPreferences) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task UpdateUserAsActive(int userId) + public async Task UpdateUserAsActive(int userId, CancellationToken ct = default) { - await _context.Set() + await context.Set() .Where(u => u.Id == userId) .ExecuteUpdateAsync(setters => setters .SetProperty(u => u.LastActiveUtc, DateTime.UtcNow) - .SetProperty(u => u.LastActive, DateTime.Now)); + .SetProperty(u => u.LastActive, DateTime.Now), ct); } /// @@ -699,14 +623,15 @@ public class UserRepository : IUserRepository /// Viewer UserId /// Search text to match against Series name /// Rating, only applies to series/chapters rated. Will show everything greater or equal to + /// /// - public async Task> GetAllReviewsForUser(int userId, int requestingUserId, string? query = null, float? ratingFilter = null) + public async Task> GetAllReviewsForUser(int userId, int requestingUserId, string? query = null, float? ratingFilter = null, CancellationToken ct = default) { var bypassPreferences = userId == requestingUserId; if (!bypassPreferences) { - var userPreferences = await _context.AppUserPreferences - .FirstOrDefaultAsync(u => u.AppUserId == userId); + var userPreferences = await context.AppUserPreferences + .FirstOrDefaultAsync(u => u.AppUserId == userId, ct); if (userPreferences?.SocialPreferences?.ShareReviews == false) { @@ -714,10 +639,10 @@ public class UserRepository : IUserRepository } } - var userRating = await _context.AppUser.GetUserAgeRestriction(requestingUserId); + var userRating = await context.AppUser.GetUserAgeRestriction(requestingUserId); // Get series-level reviews - var seriesReviews = await _context.AppUserRating + var seriesReviews = await context.AppUserRating .WhereIf(ratingFilter is > 0, r => r.HasBeenRated && r.Rating >= ratingFilter!.Value) .Include(r => r.AppUser) .Include(r => r.Series) @@ -729,11 +654,11 @@ public class UserRepository : IUserRepository .RestrictAgainstAgeRestriction(userRating, requestingUserId) .OrderBy(r => r.SeriesId) .AsSplitQuery() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); // Get chapter-level reviews - var chapterReviews = await _context.AppUserChapterRating + var chapterReviews = await context.AppUserChapterRating .Include(r => r.AppUser) .Include(r => r.Series) .Include(r => r.Chapter) @@ -743,148 +668,148 @@ public class UserRepository : IUserRepository .OrderBy(r => r.SeriesId) .ThenBy(r => r.ChapterId) .AsSplitQuery() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); // Combine and return both lists return seriesReviews.Concat(chapterReviews).ToList(); } - public async Task> GetAdminUsersAsync() + public async Task> GetAdminUsersAsync(CancellationToken ct = default) { - return (await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole)).OrderBy(u => u.CreatedUtc); + return (await userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole)).OrderBy(u => u.CreatedUtc); } - public async Task IsUserAdminAsync(AppUser? user) + public async Task IsUserAdminAsync(AppUser? user, CancellationToken ct = default) { if (user == null) return false; - return await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole); + return await userManager.IsInRoleAsync(user, PolicyConstants.AdminRole); } - public async Task> GetRoles(int userId) + public async Task> GetRoles(int userId, CancellationToken ct = default) { - var user = await _context.Users.FirstOrDefaultAsync(u => u.Id == userId); + var user = await context.Users.FirstOrDefaultAsync(u => u.Id == userId, ct); if (user == null) return ArraySegment.Empty; // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (_userManager == null) + if (userManager == null) { // userManager is null on Unit Tests only - return await _context.UserRoles + return await context.UserRoles .Where(ur => ur.UserId == userId) .Select(ur => ur.Role.Name) - .ToListAsync(); + .ToListAsync(ct); } - return await _userManager.GetRolesAsync(user); + return await userManager.GetRolesAsync(user); } - public async Task> GetRolesByAuthKey(string? apiKey) + public async Task> GetRolesByAuthKey(string? apiKey, CancellationToken ct = default) { if (string.IsNullOrEmpty(apiKey)) return ArraySegment.Empty; - var user = await _context.AppUserAuthKey + var user = await context.AppUserAuthKey .Where(k => k.Key == apiKey) .HasNotExpired() .Select(k => k.AppUser) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); if (user == null) return ArraySegment.Empty; // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (_userManager == null) + if (userManager == null) { // userManager is null on Unit Tests only - return await _context.UserRoles + return await context.UserRoles .Where(ur => ur.User.AuthKeys.Any(k => k.Key == apiKey && (k.ExpiresAtUtc == null || k.ExpiresAtUtc < DateTime.UtcNow))) .Select(ur => ur.Role.Name) - .ToListAsync(); + .ToListAsync(ct); } - return await _userManager.GetRolesAsync(user); + return await userManager.GetRolesAsync(user); } - public async Task GetUserRatingAsync(int seriesId, int userId) + public async Task GetUserRatingAsync(int seriesId, int userId, CancellationToken ct = default) { - return await _context.AppUserRating + return await context.AppUserRating .Where(r => r.SeriesId == seriesId && r.AppUserId == userId) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task GetUserChapterRatingAsync(int userId, int chapterId) + public async Task GetUserChapterRatingAsync(int userId, int chapterId, CancellationToken ct = default) { - return await _context.AppUserChapterRating + return await context.AppUserChapterRating .Where(r => r.AppUserId == userId && r.ChapterId == chapterId) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task> GetUserRatingDtosForSeriesAsync(int seriesId, int userId) + public async Task> GetUserRatingDtosForSeriesAsync(int seriesId, int userId, CancellationToken ct = default) { - var userPreferences = await _context.AppUserPreferences.ToListAsync(); + var userPreferences = await context.AppUserPreferences.ToListAsync(ct); - return await _context.AppUserRating + return await context.AppUserRating .Include(r => r.AppUser) .Where(r => r.SeriesId == seriesId) .RestrictBySocialPreferences(userId, userPreferences) .OrderBy(r => r.AppUserId == userId) .ThenBy(r => r.Rating) .AsSplitQuery() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task> GetUserRatingDtosForChapterAsync(int chapterId, int userId) + public async Task> GetUserRatingDtosForChapterAsync(int chapterId, int userId, CancellationToken ct = default) { - var userPreferences = await _context.AppUserPreferences.ToListAsync(); + var userPreferences = await context.AppUserPreferences.ToListAsync(ct); - return await _context.AppUserChapterRating + return await context.AppUserChapterRating .Include(r => r.AppUser) .Where(r => r.ChapterId == chapterId) .RestrictBySocialPreferences(userId, userPreferences) .OrderBy(r => r.AppUserId == userId) .ThenBy(r => r.Rating) .AsSplitQuery() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task GetPreferencesAsync(string username) + public async Task GetPreferencesAsync(string username, CancellationToken ct = default) { - return await _context.AppUserPreferences + return await context.AppUserPreferences .Include(p => p.AppUser) .Include(p => p.Theme) .AsSplitQuery() - .SingleOrDefaultAsync(p => p.AppUser.UserName == username); + .SingleOrDefaultAsync(p => p.AppUser.UserName == username, ct); } - public async Task> GetBookmarkDtosForSeries(int userId, int seriesId) + public async Task> GetBookmarkDtosForSeries(int userId, int seriesId, CancellationToken ct = default) { - return await _context.AppUserBookmark + return await context.AppUserBookmark .Where(x => x.AppUserId == userId && x.SeriesId == seriesId) .OrderBy(x => x.Created) .AsNoTracking() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task> GetBookmarkDtosForVolume(int userId, int volumeId) + public async Task> GetBookmarkDtosForVolume(int userId, int volumeId, CancellationToken ct = default) { - return await _context.AppUserBookmark + return await context.AppUserBookmark .Where(x => x.AppUserId == userId && x.VolumeId == volumeId) .OrderBy(x => x.Created) .AsNoTracking() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task> GetBookmarkDtosForChapter(int userId, int chapterId) + public async Task> GetBookmarkDtosForChapter(int userId, int chapterId, CancellationToken ct = default) { - return await _context.AppUserBookmark + return await context.AppUserBookmark .Where(x => x.AppUserId == userId && x.ChapterId == chapterId) .OrderBy(x => x.Created) .AsNoTracking() - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } /// @@ -892,16 +817,17 @@ public class UserRepository : IUserRepository /// /// /// Only supports SeriesNameQuery + /// /// - public async Task> GetAllBookmarkDtos(int userId, FilterV2Dto filter) + public async Task> GetAllBookmarkDtos(int userId, FilterV2Dto filter, CancellationToken ct = default) { - var query = _context.AppUserBookmark + var query = context.AppUserBookmark .Where(x => x.AppUserId == userId) .OrderBy(x => x.Created) .AsNoTracking(); - var filterSeriesQuery = query.Join(_context.Series, b => b.SeriesId, s => s.Id, - (bookmark, series) => new BookmarkSeriesPair() + var filterSeriesQuery = query.Join(context.Series, b => b.SeriesId, s => s.Id, + (bookmark, series) => new BookmarkSeriesPair { Bookmark = bookmark, Series = series @@ -913,8 +839,8 @@ public class UserRepository : IUserRepository return await ApplyLimit(filterSeriesQuery .Sort(filter.SortOptions) .AsSplitQuery(), filter.LimitTo) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } var queryString = filterStatement.Value.ToNormalized(); @@ -968,8 +894,8 @@ public class UserRepository : IUserRepository return await ApplyLimit(filterSeriesQuery .Sort(filter.SortOptions) .AsSplitQuery(), filter.LimitTo) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } private static IQueryable ApplyLimit(IQueryable query, int limit) @@ -977,44 +903,44 @@ public class UserRepository : IUserRepository return limit <= 0 ? query : query.Take(limit); } - public async Task GetUserDtoByAuthKeyAsync(string authKey) + public async Task GetUserDtoByAuthKeyAsync(string authKey, CancellationToken ct = default) { if (string.IsNullOrEmpty(authKey)) return null; - return await _context.AppUserAuthKey + return await context.AppUserAuthKey .Where(k => k.Key == authKey) .HasNotExpired() .Include(k => k.AppUser) .ThenInclude(u => u.UserRoles) .ThenInclude(ur => ur.Role) .Select(k => k.AppUser) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(ct); } - public async Task GetUserIdByAuthKeyAsync(string authKey) + public async Task GetUserIdByAuthKeyAsync(string authKey, CancellationToken ct = default) { if (string.IsNullOrEmpty(authKey)) return 0; - return await _context.AppUserAuthKey + return await context.AppUserAuthKey .Where(k => k.Key == authKey) .HasNotExpired() .Select(k => k.AppUserId) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task GetUserDtoById(int userId) + public async Task GetUserDtoById(int userId, CancellationToken ct = default) { - return await _context.AppUser + return await context.AppUser .Where(u => u.Id == userId) - .ProjectTo(_mapper.ConfigurationProvider) - .FirstOrDefaultAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(ct); } - public async Task> GetEmailConfirmedMemberDtosAsync(bool emailConfirmed = true) + public async Task> GetEmailConfirmedMemberDtosAsync(bool emailConfirmed = true, CancellationToken ct = default) { - return await _context.Users + return await context.Users .Where(u => (emailConfirmed && u.EmailConfirmed) || !emailConfirmed) .Include(x => x.Libraries) .Include(r => r.UserRoles) @@ -1048,84 +974,69 @@ public class UserRepository : IUserRepository }) .AsSplitQuery() .AsNoTracking() - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetCoverImageAsync(int userId, int requestingUserId) + public Task GetCoverImageAsync(int userId, CancellationToken ct = default) { - // TODO: .NET JSON Support - // return await _context.AppUser - // .Include(c => c.UserPreferences) - // .Where(c => c.Id == userId && c.UserPreferences.SocialPreferences.ShareProfile) - // .Select(c => c.CoverImage) - // .FirstOrDefaultAsync(); - - var user = await _context.AppUser - .Include(c => c.UserPreferences) - .Where(c => c.Id == userId) - .Select(c => new { c.CoverImage, c.UserPreferences }) - .FirstOrDefaultAsync(); - - if (user?.UserPreferences?.SocialPreferences?.ShareProfile == true || userId == requestingUserId) - { - return user.CoverImage; - } - - return null; + return context.AppUser + .Where(u => u.Id == userId) + .Select(u => u.CoverImage) + .FirstOrDefaultAsync(ct); } - public async Task GetPersonCoverImageAsync(int personId) + public async Task GetPersonCoverImageAsync(int personId, CancellationToken ct = default) { - return await _context.Person + return await context.Person .Where(p => p.Id == personId) .Select(p => p.CoverImage) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task> GetAuthKeysForUserId(int userId) + public async Task> GetAuthKeysForUserId(int userId, CancellationToken ct = default) { - return await _context.AppUserAuthKey + return await context.AppUserAuthKey .Where(k => k.AppUserId == userId) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task> GetAllAuthKeysDtosWithExpiration() + public async Task> GetAllAuthKeysDtosWithExpiration(CancellationToken ct = default) { - return await _context.AppUserAuthKey + return await context.AppUserAuthKey .Where(k => k.ExpiresAtUtc != null) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(ct); } - public async Task GetAuthKeyById(int authKeyId) + public async Task GetAuthKeyById(int authKeyId, CancellationToken ct = default) { - return await _context.AppUserAuthKey + return await context.AppUserAuthKey .Where(k => k.Id == authKeyId) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task GetAuthKeyExpiration(string authKey, int userId) + public async Task GetAuthKeyExpiration(string authKey, int userId, CancellationToken ct = default) { - return await _context.AppUserAuthKey + return await context.AppUserAuthKey .Where(k => k.Key == authKey && k.AppUserId == userId) .Select(k => k.ExpiresAtUtc) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); } - public async Task GetSocialPreferencesForUser(int userId) + public async Task GetSocialPreferencesForUser(int userId, CancellationToken ct = default) { - return await _context.AppUserPreferences + return await context.AppUserPreferences .Where(p => p.AppUserId == userId) .Select(p => p.SocialPreferences) - .FirstAsync(); + .FirstAsync(ct); } - public async Task GetPreferencesForUser(int userId) + public async Task GetPreferencesForUser(int userId, CancellationToken ct = default) { - return await _context.AppUserPreferences + return await context.AppUserPreferences .Where(p => p.AppUserId == userId) - .FirstAsync(); + .FirstAsync(ct); } /// @@ -1133,12 +1044,12 @@ public class UserRepository : IUserRepository /// /// /// - public async Task GetOpdsPreferences(int userId) + public async Task GetOpdsPreferences(int userId, CancellationToken ct = default) { - return await _context.AppUserPreferences + return await context.AppUserPreferences .Where(p => p.AppUserId == userId) .Select(p => p.OpdsPreferences) .AsNoTracking() - .FirstAsync(); + .FirstAsync(ct); } } diff --git a/Kavita.Database/Repositories/UserTableOfContentRepository.cs b/Kavita.Database/Repositories/UserTableOfContentRepository.cs new file mode 100644 index 000000000..63500eae1 --- /dev/null +++ b/Kavita.Database/Repositories/UserTableOfContentRepository.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Kavita.API.Repositories; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities.User; +using Microsoft.EntityFrameworkCore; + +namespace Kavita.Database.Repositories; + +public class UserTableOfContentRepository(DataContext context, IMapper mapper) : IUserTableOfContentRepository +{ + public void Attach(AppUserTableOfContent toc) + { + context.AppUserTableOfContent.Attach(toc); + } + + public void Remove(AppUserTableOfContent toc) + { + context.AppUserTableOfContent.Remove(toc); + } + + public async Task IsUnique(int userId, int chapterId, int page, string title) + { + return await context.AppUserTableOfContent.AnyAsync(t => + t.AppUserId == userId && t.PageNumber == page && t.Title == title && t.ChapterId == chapterId); + } + + public async Task> GetPersonalToC(int userId, int chapterId) + { + return await context.AppUserTableOfContent + .Where(t => t.AppUserId == userId && t.ChapterId == chapterId) + .ProjectTo(mapper.ConfigurationProvider) + .OrderBy(t => t.PageNumber) + .ToListAsync(); + } + + public async Task> GetPersonalToCForPage(int userId, int chapterId, int page) + { + return await context.AppUserTableOfContent + .Where(t => t.AppUserId == userId && t.ChapterId == chapterId && t.PageNumber == page) + .ProjectTo(mapper.ConfigurationProvider) + .OrderBy(t => t.PageNumber) + .ToListAsync(); + } + + public async Task Get(int userId,int chapterId, int pageNum, string title) + { + return await context.AppUserTableOfContent + .Where(t => t.AppUserId == userId && t.ChapterId == chapterId && t.PageNumber == pageNum && t.Title == title) + .FirstOrDefaultAsync(); + } +} diff --git a/API/Data/Repositories/VolumeRepository.cs b/Kavita.Database/Repositories/VolumeRepository.cs similarity index 59% rename from API/Data/Repositories/VolumeRepository.cs rename to Kavita.Database/Repositories/VolumeRepository.cs index 85a512c49..9aedf7df0 100644 --- a/API/Data/Repositories/VolumeRepository.cs +++ b/Kavita.Database/Repositories/VolumeRepository.cs @@ -1,155 +1,95 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.DTOs; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Extensions.QueryExtensions; using AutoMapper; +using Kavita.API.Repositories; +using Kavita.Database.Extensions; +using Kavita.Models.DTOs; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Extensions; using Microsoft.EntityFrameworkCore; -namespace API.Data.Repositories; -#nullable enable +namespace Kavita.Database.Repositories; -[Flags] -public enum VolumeIncludes +public class VolumeRepository(DataContext context, IMapper mapper) : IVolumeRepository { - None = 1, - Chapters = 2, - People = 4, - Tags = 8, - /// - /// This will include Chapters by default - /// - Files = 16 -} - -public interface IVolumeRepository -{ - void Add(Volume volume); - void Update(Volume volume); - void Remove(Volume volume); - void Remove(IList volumes); - Task GetFilesizeForVolumeAsync(int volumeId); - Task> GetFilesizeForVolumesAsync(IList volumeIds); - Task> GetFilesForVolume(int volumeId); - Task GetVolumeCoverImageAsync(int volumeId); - Task> GetChapterIdsByVolumeIds(IReadOnlyList volumeIds); - Task> GetVolumesDtoAsync(int seriesId, int userId, VolumeIncludes includes = VolumeIncludes.Chapters); - Task GetVolumeByIdAsync(int volumeId, VolumeIncludes includes = VolumeIncludes.Files); - Task GetVolumeDtoAsync(int volumeId, int userId); - Task> GetVolumesForSeriesAsync(IList seriesIds, bool includeChapters = false); - Task> GetVolumes(int seriesId); - Task> GetVolumesById(IList volumeIds, VolumeIncludes includes = VolumeIncludes.None); - Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat); - Task> GetCoverImagesForLockedVolumesAsync(); -} -public class VolumeRepository : IVolumeRepository -{ - private readonly DataContext _context; - private readonly IMapper _mapper; - - public VolumeRepository(DataContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - public void Add(Volume volume) { - _context.Volume.Add(volume); + context.Volume.Add(volume); } public void Update(Volume volume) { - _context.Entry(volume).State = EntityState.Modified; + context.Entry(volume).State = EntityState.Modified; } public void Remove(Volume volume) { - _context.Volume.Remove(volume); + context.Volume.Remove(volume); } public void Remove(IList volumes) { - _context.Volume.RemoveRange(volumes); - } - - public async Task GetFilesizeForVolumeAsync(int volumeId) - { - return await _context.Chapter - .Where(c => volumeId == c.VolumeId) - .Include(c => c.Files) - .SelectMany(c => c.Files) - .SumAsync(f => f.Bytes); - } - - public async Task> GetFilesizeForVolumesAsync(IList volumeIds) - { - return await volumeIds.BatchToDictionaryAsync(50, batch => - _context.Chapter - .Where(c => batch.Contains(c.VolumeId)) - .GroupBy(c => c.VolumeId) - .Select(g => new - { - VolumeId = g.Key, - TotalBytes = g.SelectMany(c => c.Files).Sum(f => f.Bytes) - }) - .ToDictionaryAsync(x => x.VolumeId, x => x.TotalBytes)); + context.Volume.RemoveRange(volumes); } /// /// Returns a list of non-tracked files for a given volume. /// /// + /// /// - public async Task> GetFilesForVolume(int volumeId) + public async Task> GetFilesForVolume(int volumeId, CancellationToken ct = default) { - return await _context.Chapter + return await context.Chapter .Where(c => volumeId == c.VolumeId) .Include(c => c.Files) .SelectMany(c => c.Files) .AsSplitQuery() .AsNoTracking() - .ToListAsync(); + .ToListAsync(ct); } /// /// Returns the cover image file for the given volume /// /// + /// /// - public async Task GetVolumeCoverImageAsync(int volumeId) + public async Task GetVolumeCoverImageAsync(int volumeId, CancellationToken ct = default) { - return await _context.Volume + return await context.Volume .Where(v => v.Id == volumeId) .Select(v => v.CoverImage) - .SingleOrDefaultAsync(); + .SingleOrDefaultAsync(ct); } /// /// Returns all chapter Ids belonging to a list of Volume Ids /// /// + /// /// - public async Task> GetChapterIdsByVolumeIds(IReadOnlyList volumeIds) + public async Task> GetChapterIdsByVolumeIds(IReadOnlyList volumeIds, CancellationToken ct = default) { - return await _context.Chapter + return await context.Chapter .Where(c => volumeIds.Contains(c.VolumeId)) .Select(c => c.Id) - .ToListAsync(); + .ToListAsync(ct); } /// - /// Returns all volumes that contain a seriesId in passed array. + /// Returns all volumes that contain a seriesId in a passed array. /// /// /// Include chapter entities + /// /// - public async Task> GetVolumesForSeriesAsync(IList seriesIds, bool includeChapters = false) + public async Task> GetVolumesForSeriesAsync(IList seriesIds, bool includeChapters = false, + CancellationToken ct = default) { - var query = _context.Volume + var query = context.Volume .Where(v => seriesIds.Contains(v.SeriesId)); if (includeChapters) @@ -158,7 +98,7 @@ public class VolumeRepository : IVolumeRepository .Includes(VolumeIncludes.Chapters) .AsSplitQuery(); } - var volumes = await query.ToListAsync(); + var volumes = await query.ToListAsync(ct); foreach (var volume in volumes) { @@ -173,53 +113,59 @@ public class VolumeRepository : IVolumeRepository /// /// /// + /// /// - public async Task GetVolumeDtoAsync(int volumeId, int userId) + public async Task GetVolumeDtoAsync(int volumeId, int userId, CancellationToken ct = default) { - return await _context.Volume + return await context.Volume .Where(vol => vol.Id == volumeId) .Includes(VolumeIncludes.Chapters | VolumeIncludes.Files) .AsSplitQuery() .OrderBy(v => v.MinNumber) - .ProjectToWithProgress(_mapper, userId) - .FirstOrDefaultAsync(vol => vol.Id == volumeId); + .ProjectToWithProgress(mapper, userId) + .FirstOrDefaultAsync(vol => vol.Id == volumeId, ct); } /// /// Returns the full Volumes including Chapters and Files for a given series /// /// + /// /// - public async Task> GetVolumes(int seriesId) + public async Task> GetVolumes(int seriesId, CancellationToken ct = default) { - return await _context.Volume + return await context.Volume .Where(vol => vol.SeriesId == seriesId) .Includes(VolumeIncludes.Chapters | VolumeIncludes.Files) .AsSplitQuery() .OrderBy(vol => vol.MinNumber) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetVolumesById(IList volumeIds, VolumeIncludes includes = VolumeIncludes.None) + public async Task> GetVolumesById(IList volumeIds, VolumeIncludes includes = VolumeIncludes.None, + CancellationToken ct = default) { - return await _context.Volume + return await context.Volume .Where(vol => volumeIds.Contains(vol.Id)) .Includes(includes) .AsSplitQuery() .OrderBy(vol => vol.MinNumber) - .ToListAsync(); + .ToListAsync(ct); } /// /// Returns a single volume with Chapter and Files /// /// + /// + /// /// - public async Task GetVolumeByIdAsync(int volumeId, VolumeIncludes includes = VolumeIncludes.Files) + public async Task GetVolumeByIdAsync(int volumeId, VolumeIncludes includes = VolumeIncludes.Files, + CancellationToken ct = default) { - return await _context.Volume + return await context.Volume .Includes(includes) .AsSplitQuery() - .SingleOrDefaultAsync(vol => vol.Id == volumeId); + .SingleOrDefaultAsync(vol => vol.Id == volumeId, ct); } @@ -228,39 +174,67 @@ public class VolumeRepository : IVolumeRepository ///
/// /// + /// + /// /// - public async Task> GetVolumesDtoAsync(int seriesId, int userId, VolumeIncludes includes = VolumeIncludes.Chapters) + public async Task> GetVolumesDtoAsync(int seriesId, int userId, + VolumeIncludes includes = VolumeIncludes.Chapters, CancellationToken ct = default) { - return await _context.Volume + return await context.Volume .Where(vol => vol.SeriesId == seriesId) .Includes(includes) .OrderBy(volume => volume.MinNumber) - .ProjectToWithProgress(_mapper, userId) + .ProjectToWithProgress(mapper, userId) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat) + public async Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, + CancellationToken ct = default) { var extension = encodeFormat.GetExtension(); - return await _context.Volume + return await context.Volume .Includes(VolumeIncludes.Chapters) .Where(c => !string.IsNullOrEmpty(c.CoverImage) && !c.CoverImage.EndsWith(extension)) .AsSplitQuery() - .ToListAsync(); + .ToListAsync(ct); } /// /// Returns cover images for locked chapters /// + /// /// - public async Task> GetCoverImagesForLockedVolumesAsync() + public async Task> GetCoverImagesForLockedVolumesAsync(CancellationToken ct = default) { - return (await _context.Volume + return (await context.Volume .Where(c => c.CoverImageLocked) .Select(c => c.CoverImage) .Where(t => !string.IsNullOrEmpty(t)) - .ToListAsync())!; + .ToListAsync(ct))!; + } + + public async Task GetFilesizeForVolumeAsync(int volumeId, CancellationToken ct = default) + { + return await context.Chapter + .Where(c => volumeId == c.VolumeId) + .Include(c => c.Files) + .SelectMany(c => c.Files) + .SumAsync(f => f.Bytes, cancellationToken: ct); + } + + public async Task> GetFilesizeForVolumesAsync(IList volumeIds, CancellationToken ct = default) + { + return await volumeIds.BatchToDictionaryAsync(50, batch => + context.Chapter + .Where(c => batch.Contains(c.VolumeId)) + .GroupBy(c => c.VolumeId) + .Select(g => new + { + VolumeId = g.Key, + TotalBytes = g.SelectMany(c => c.Files).Sum(f => f.Bytes) + }) + .ToDictionaryAsync(x => x.VolumeId, x => x.TotalBytes, cancellationToken: ct)); } } diff --git a/API/Data/Seed.cs b/Kavita.Database/Seed.cs similarity index 57% rename from API/Data/Seed.cs rename to Kavita.Database/Seed.cs index a7afc07af..e2895c6cb 100644 --- a/API/Data/Seed.cs +++ b/Kavita.Database/Seed.cs @@ -1,257 +1,30 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Text.Json; using System.Threading.Tasks; -using API.Constants; -using API.Data.Repositories; -using API.DTOs.Settings; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Enums.Font; -using API.Entities.Enums.Theme; -using API.Entities.Enums.User; -using API.Entities.MetadataMatching; -using API.Entities.User; -using API.Extensions; -using API.Helpers; -using API.Services; -using API.Services.Tasks; -using API.Services.Tasks.Scanner.Parser; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; using Kavita.Common; using Kavita.Common.EnvironmentInfo; +using Kavita.Models; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.MetadataMatching; +using Kavita.Models.Entities.User; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; -namespace API.Data; +namespace Kavita.Database; public static class Seed { - /// - /// Generated on Startup. Seed.SeedSettings must run before - /// - public static ImmutableArray DefaultSettings; - - public static readonly ImmutableArray DefaultHighlightSlots = - [ - new() - { - Id = 1, - SlotNumber = 0, - Color = new RgbaColor { R = 0, G = 255, B = 255, A = 0.4f } - }, - new() - { - Id = 2, - SlotNumber = 1, - Color = new RgbaColor { R = 0, G = 255, B = 0, A = 0.4f } - }, - new() - { - Id = 3, - SlotNumber = 2, - Color = new RgbaColor { R = 255, G = 255, B = 0, A = 0.4f } - }, - new() - { - Id = 4, - SlotNumber = 3, - Color = new RgbaColor { R = 255, G = 165, B = 0, A = 0.4f } - }, - new() - { - Id = 5, - SlotNumber = 4, - Color = new RgbaColor { R = 255, G = 0, B = 255, A = 0.4f } - } - ]; - - public static readonly ImmutableArray DefaultFonts = - [ - new () - { - Name = FontService.DefaultFont, - NormalizedName = Parser.Normalize(FontService.DefaultFont), - Provider = FontProvider.System, - FileName = string.Empty, - }, - new () - { - Name = "Merriweather", - NormalizedName = Parser.Normalize("Merriweather"), - Provider = FontProvider.System, - FileName = "Merriweather-Regular.woff2", - }, - new () - { - Name = "EB Garamond", - NormalizedName = Parser.Normalize("EB Garamond"), - Provider = FontProvider.System, - FileName = "EBGaramond-VariableFont_wght.woff2", - }, - new () - { - Name = "Fira Sans", - NormalizedName = Parser.Normalize("Fira Sans"), - Provider = FontProvider.System, - FileName = "FiraSans-Regular.woff2", - }, - new () - { - Name = "Lato", - NormalizedName = Parser.Normalize("Lato"), - Provider = FontProvider.System, - FileName = "Lato-Regular.woff2", - }, - new () - { - Name = "Libre Baskerville", - NormalizedName = Parser.Normalize("Libre Baskerville"), - Provider = FontProvider.System, - FileName = "LibreBaskerville-Regular.woff2", - }, - new () - { - Name = "Nanum Gothic", - NormalizedName = Parser.Normalize("Nanum Gothic"), - Provider = FontProvider.System, - FileName = "NanumGothic-Regular.woff2", - }, - new () - { - Name = "Open Dyslexic", - NormalizedName = Parser.Normalize("Open Dyslexic"), - Provider = FontProvider.System, - FileName = "OpenDyslexic-Regular.woff2", - }, - new () - { - Name = "RocknRoll One", - NormalizedName = Parser.Normalize("RocknRoll One"), - Provider = FontProvider.System, - FileName = "RocknRollOne-Regular.woff2", - }, - new () - { - Name = "Fast Font Serif", - NormalizedName = Parser.Normalize("Fast Font Serif"), - Provider = FontProvider.System, - FileName = "Fast_Serif.woff2", - }, - new () - { - Name = "Fast Font Sans", - NormalizedName = Parser.Normalize("Fast Font Sans"), - Provider = FontProvider.System, - FileName = "Fast_Sans.woff2", - } - ]; - - public static readonly ImmutableArray DefaultThemes = [ - ..new List - { - new() - { - Name = "Dark", - NormalizedName = "Dark".ToNormalized(), - Provider = ThemeProvider.System, - FileName = "dark.scss", - IsDefault = true, - Description = "Default theme shipped with Kavita" - } - }.ToArray() - ]; - - public static readonly ImmutableArray DefaultStreams = [ - ..new List - { - new() - { - Name = "on-deck", - StreamType = DashboardStreamType.OnDeck, - Order = 0, - IsProvided = true, - Visible = true - }, - new() - { - Name = "recently-updated", - StreamType = DashboardStreamType.RecentlyUpdated, - Order = 1, - IsProvided = true, - Visible = true - }, - new() - { - Name = "newly-added", - StreamType = DashboardStreamType.NewlyAdded, - Order = 2, - IsProvided = true, - Visible = true - }, - new() - { - Name = "more-in-genre", - StreamType = DashboardStreamType.MoreInGenre, - Order = 3, - IsProvided = true, - Visible = false - }, - }.ToArray() - ]; - - public static readonly ImmutableArray DefaultSideNavStreams = - [ - new() - { - Name = "want-to-read", - StreamType = SideNavStreamType.WantToRead, - Order = 1, - IsProvided = true, - Visible = true - }, new() - { - Name = "collections", - StreamType = SideNavStreamType.Collections, - Order = 2, - IsProvided = true, - Visible = true - }, new() - { - Name = "reading-lists", - StreamType = SideNavStreamType.ReadingLists, - Order = 3, - IsProvided = true, - Visible = true - }, new() - { - Name = "bookmarks", - StreamType = SideNavStreamType.Bookmarks, - Order = 4, - IsProvided = true, - Visible = true - }, new() - { - Name = "all-series", - StreamType = SideNavStreamType.AllSeries, - Order = 5, - IsProvided = true, - Visible = true - }, - new() - { - Name = "browse-authors", - StreamType = SideNavStreamType.BrowsePeople, - Order = 6, - IsProvided = true, - Visible = true - } - ]; - public static async Task SeedRoles(RoleManager roleManager) { @@ -273,11 +46,11 @@ public static class Seed } } - public static async Task SeedThemes(DataContext context) + public static async Task SeedThemes(IDataContext context) { await context.Database.EnsureCreatedAsync(); - foreach (var theme in DefaultThemes) + foreach (var theme in Defaults.DefaultThemes) { var existing = await context.SiteTheme.FirstOrDefaultAsync(s => s.Name.Equals(theme.Name)); if (existing == null) @@ -289,11 +62,11 @@ public static class Seed await context.SaveChangesAsync(); } - public static async Task SeedFonts(DataContext context) + public static async Task SeedFonts(IDataContext context) { await context.Database.EnsureCreatedAsync(); - foreach (var font in DefaultFonts) + foreach (var font in Defaults.DefaultFonts) { var existing = await context.EpubFont.FirstOrDefaultAsync(f => f.Name.Equals(font.Name)); if (existing == null) @@ -312,7 +85,7 @@ public static class Seed { if (user.DashboardStreams.Count != 0) continue; user.DashboardStreams ??= []; - foreach (var defaultStream in DefaultStreams) + foreach (var defaultStream in Defaults.DefaultStreams) { var newStream = new AppUserDashboardStream { @@ -336,7 +109,7 @@ public static class Seed foreach (var user in allUsers) { user.SideNavStreams ??= []; - foreach (var defaultStream in DefaultSideNavStreams) + foreach (var defaultStream in Defaults.DefaultSideNavStreams) { if (user.SideNavStreams.Any(s => s.Name == defaultStream.Name && s.StreamType == defaultStream.StreamType)) continue; var newStream = new AppUserSideNavStream() @@ -362,16 +135,16 @@ public static class Seed { if (user.UserPreferences.BookReaderHighlightSlots.Any()) break; - user.UserPreferences.BookReaderHighlightSlots = DefaultHighlightSlots.ToList(); + user.UserPreferences.BookReaderHighlightSlots = Defaults.DefaultHighlightSlots.ToList(); unitOfWork.UserRepository.Update(user); } await unitOfWork.CommitAsync(); } - public static async Task SeedSettings(DataContext context, IDirectoryService directoryService) + public static async Task SeedSettings(IDataContext context, IDirectoryService directoryService) { await context.Database.EnsureCreatedAsync(); - DefaultSettings = [ + Defaults.DefaultSettings = [ ..new List() { new() {Key = ServerSettingKey.CacheDirectory, Value = directoryService.CacheDirectory}, @@ -381,7 +154,7 @@ public static class Seed new() {Key = ServerSettingKey.LoggingLevel, Value = "Debug"}, new() { - Key = ServerSettingKey.BackupDirectory, Value = Path.GetFullPath(DirectoryService.BackupDirectory) + Key = ServerSettingKey.BackupDirectory, Value = Path.GetFullPath(directoryService.BackupDirectory) }, new() { @@ -426,7 +199,7 @@ public static class Seed }.ToArray() ]; - foreach (var defaultSetting in DefaultSettings) + foreach (var defaultSetting in Defaults.DefaultSettings) { var existing = await context.ServerSetting.FirstOrDefaultAsync(s => s.Key == defaultSetting.Key); if (existing == null) @@ -445,7 +218,7 @@ public static class Seed (await context.ServerSetting.FirstAsync(s => s.Key == ServerSettingKey.CacheDirectory)).Value = directoryService.CacheDirectory + string.Empty; (await context.ServerSetting.FirstAsync(s => s.Key == ServerSettingKey.BackupDirectory)).Value = - DirectoryService.BackupDirectory + string.Empty; + directoryService.BackupDirectory + string.Empty; (await context.ServerSetting.FirstAsync(s => s.Key == ServerSettingKey.CacheSize)).Value = Configuration.CacheSize + string.Empty; @@ -455,7 +228,7 @@ public static class Seed await context.SaveChangesAsync(); } - public static async Task SetOidcSettingsFromDisk(DataContext context) + public static async Task SetOidcSettingsFromDisk(IDataContext context) { var oidcSettingEntry = await context.ServerSetting .FirstOrDefaultAsync(setting => setting.Key == ServerSettingKey.OidcConfiguration); @@ -472,7 +245,7 @@ public static class Seed oidcSettingEntry.Value = JsonSerializer.Serialize(storedOidcSettings); } - public static async Task SeedMetadataSettings(DataContext context) + public static async Task SeedMetadataSettings(IDataContext context) { await context.Database.EnsureCreatedAsync(); @@ -506,27 +279,4 @@ public static class Seed await context.SaveChangesAsync(); } - - public static List CreateDefaultAuthKeys() - { - return - [ - new AppUserAuthKey() - { - Name = AuthKeyHelper.OpdsKeyName, - Key = AuthKeyHelper.GenerateKey(32), - CreatedAtUtc = DateTime.UtcNow, - ExpiresAtUtc = null, - Provider = AuthKeyProvider.System, - }, - new AppUserAuthKey() - { - Name = AuthKeyHelper.ImageOnlyKeyName, - Key = AuthKeyHelper.GenerateKey(32), - CreatedAtUtc = DateTime.UtcNow, - ExpiresAtUtc = null, - Provider = AuthKeyProvider.System, - } - ]; - } } diff --git a/API/Data/UnitOfWork.cs b/Kavita.Database/UnitOfWork.cs similarity index 72% rename from API/Data/UnitOfWork.cs rename to Kavita.Database/UnitOfWork.cs index 7f30a0a73..fee29fd91 100644 --- a/API/Data/UnitOfWork.cs +++ b/Kavita.Database/UnitOfWork.cs @@ -1,47 +1,15 @@ using System; +using System.Threading; using System.Threading.Tasks; -using API.Data.Repositories; -using API.Entities; using AutoMapper; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.Database.Repositories; +using Kavita.Models.Entities.User; using Microsoft.AspNetCore.Identity; -namespace API.Data; +namespace Kavita.Database; -public interface IUnitOfWork -{ - DataContext DataContext { get; } - ISeriesRepository SeriesRepository { get; } - IUserRepository UserRepository { get; } - ILibraryRepository LibraryRepository { get; } - IVolumeRepository VolumeRepository { get; } - ISettingsRepository SettingsRepository { get; } - IAppUserProgressRepository AppUserProgressRepository { get; } - ICollectionTagRepository CollectionTagRepository { get; } - IChapterRepository ChapterRepository { get; } - IReadingListRepository ReadingListRepository { get; } - ISeriesMetadataRepository SeriesMetadataRepository { get; } - IPersonRepository PersonRepository { get; } - IGenreRepository GenreRepository { get; } - ITagRepository TagRepository { get; } - ISiteThemeRepository SiteThemeRepository { get; } - IMangaFileRepository MangaFileRepository { get; } - IDeviceRepository DeviceRepository { get; } - IMediaErrorRepository MediaErrorRepository { get; } - IScrobbleRepository ScrobbleRepository { get; } - IUserTableOfContentRepository UserTableOfContentRepository { get; } - IAppUserSmartFilterRepository AppUserSmartFilterRepository { get; } - IAppUserExternalSourceRepository AppUserExternalSourceRepository { get; } - IExternalSeriesMetadataRepository ExternalSeriesMetadataRepository { get; } - IEmailHistoryRepository EmailHistoryRepository { get; } - IAppUserReadingProfileRepository AppUserReadingProfileRepository { get; } - IAnnotationRepository AnnotationRepository { get; } - IEpubFontRepository EpubFontRepository { get; } - IReadingSessionRepository ReadingSessionRepository { get; } - bool Commit(); - Task CommitAsync(); - bool HasChanges(); - Task RollbackAsync(); -} public class UnitOfWork : IUnitOfWork { @@ -82,12 +50,13 @@ public class UnitOfWork : IUnitOfWork AnnotationRepository = new AnnotationRepository(_context, _mapper); EpubFontRepository = new EpubFontRepository(_context, _mapper); ReadingSessionRepository = new ReadingSessionRepository(_context, _mapper); + ClientDeviceRepository = new ClientDeviceRepository(_context, _mapper); } /// /// This is here for Scanner only. Don't use otherwise. /// - public DataContext DataContext => _context; + public IDataContext DataContext => _context; public ISeriesRepository SeriesRepository { get; } public IUserRepository UserRepository { get; } public ILibraryRepository LibraryRepository { get; } @@ -115,6 +84,7 @@ public class UnitOfWork : IUnitOfWork public IAnnotationRepository AnnotationRepository { get; } public IEpubFontRepository EpubFontRepository { get; } public IReadingSessionRepository ReadingSessionRepository { get; } + public IClientDeviceRepository ClientDeviceRepository { get; } /// /// Commits changes to the DB. Completes the open transaction. @@ -124,13 +94,15 @@ public class UnitOfWork : IUnitOfWork { return _context.SaveChanges() > 0; } + /// /// Commits changes to the DB. Completes the open transaction. /// + /// /// - public async Task CommitAsync() + public async Task CommitAsync(CancellationToken ct = default) { - return await _context.SaveChangesAsync() > 0; + return await _context.SaveChangesAsync(ct) > 0; } /// @@ -155,12 +127,13 @@ public class UnitOfWork : IUnitOfWork /// /// Rollback transaction /// + /// /// - public async Task RollbackAsync() + public async Task RollbackAsync(CancellationToken ct = default) { try { - await _context.Database.RollbackTransactionAsync(); + await _context.Database.RollbackTransactionAsync(ct); } catch (Exception) { diff --git a/API.Tests/Extensions/EncodeFormatExtensionsTests.cs b/Kavita.Models.Tests/Extensions/EncodeFormatExtensionsTests.cs similarity index 67% rename from API.Tests/Extensions/EncodeFormatExtensionsTests.cs rename to Kavita.Models.Tests/Extensions/EncodeFormatExtensionsTests.cs index a71b2e754..109de77ab 100644 --- a/API.Tests/Extensions/EncodeFormatExtensionsTests.cs +++ b/Kavita.Models.Tests/Extensions/EncodeFormatExtensionsTests.cs @@ -1,11 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using API.Entities.Enums; -using API.Extensions; -using Xunit; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Extensions; -namespace API.Tests.Extensions; +namespace Kavita.Models.Tests.Extensions; public class EncodeFormatExtensionsTests { @@ -21,7 +17,7 @@ public class EncodeFormatExtensionsTests }; // Act & Assert - foreach (var format in Enum.GetValues(typeof(EncodeFormat)).Cast()) + foreach (var format in Enum.GetValues()) { var extension = format.GetExtension(); Assert.Equal(expectedExtensions[format], extension); diff --git a/API.Tests/Extensions/EnumExtensionTests.cs b/Kavita.Models.Tests/Extensions/EnumExtensionTests.cs similarity index 78% rename from API.Tests/Extensions/EnumExtensionTests.cs rename to Kavita.Models.Tests/Extensions/EnumExtensionTests.cs index 0e8b03f09..94dcc37d1 100644 --- a/API.Tests/Extensions/EnumExtensionTests.cs +++ b/Kavita.Models.Tests/Extensions/EnumExtensionTests.cs @@ -1,10 +1,7 @@ -#nullable enable -using System; -using API.Entities.Enums; -using API.Extensions; -using Xunit; +using Kavita.Common.Extensions; +using Kavita.Models.Entities.Enums; -namespace API.Tests.Extensions; +namespace Kavita.Models.Tests.Extensions; public class EnumExtensionTests { diff --git a/Kavita.Models.Tests/Kavita.Models.Tests.csproj b/Kavita.Models.Tests/Kavita.Models.Tests.csproj new file mode 100644 index 000000000..5658ef26a --- /dev/null +++ b/Kavita.Models.Tests/Kavita.Models.Tests.csproj @@ -0,0 +1,31 @@ + + + + net10.0 + enable + enable + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + \ No newline at end of file diff --git a/API/Data/AutoMapper/AutoMapperChapterProfile.cs b/Kavita.Models/AutoMapper/AutoMapperChapterProfile.cs similarity index 83% rename from API/Data/AutoMapper/AutoMapperChapterProfile.cs rename to Kavita.Models/AutoMapper/AutoMapperChapterProfile.cs index 639235f62..70e8f6f2e 100644 --- a/API/Data/AutoMapper/AutoMapperChapterProfile.cs +++ b/Kavita.Models/AutoMapper/AutoMapperChapterProfile.cs @@ -1,11 +1,11 @@ using System; using System.Linq; -using API.DTOs; -using API.Entities; -using API.Entities.Enums; using AutoMapper; +using Kavita.Models.DTOs; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; -namespace API.Data.AutoMapper; +namespace Kavita.Models.AutoMapper; /// /// Maps Chapter entities to ChapterDto with user progress attached at the DB level via JOIN. @@ -14,10 +14,26 @@ public class AutoMapperChapterProfile : Profile { public AutoMapperChapterProfile() { - int userId = 0; // Placeholder, will be replaced at runtime + int userId = 0; // Placeholder will be replaced at runtime CreateMap() - // Progress fields (previously in AddChapterModifiers) + .MapChapterBase(userId); + + CreateMap() + .MapChapterBase(userId) + .ForMember(dest => dest.SeriesId, opt => opt.MapFrom(src => src.Volume.SeriesId)) + .ForMember(dest => dest.VolumeTitle, opt => opt.MapFrom(src => src.Volume.Name)) + .ForMember(dest => dest.LibraryId, opt => opt.MapFrom(src => src.Volume.Series.LibraryId)) + .ForMember(dest => dest.LibraryType, opt => opt.MapFrom(src => src.Volume.Series.Library.Type)); + } +} + +internal static class AutoMapperChapterProfileBaseExtensions +{ + public static IMappingExpression MapChapterBase(this IMappingExpression mapping, int userId) + where TDest: ChapterDto + { + return mapping .ForMember(dest => dest.PagesRead, opt => opt.MapFrom(src => src.UserProgress diff --git a/API/Data/AutoMapper/AutoMapperProfiles.cs b/Kavita.Models/AutoMapper/AutoMapperProfiles.cs similarity index 90% rename from API/Data/AutoMapper/AutoMapperProfiles.cs rename to Kavita.Models/AutoMapper/AutoMapperProfiles.cs index 97966e257..881430c67 100644 --- a/API/Data/AutoMapper/AutoMapperProfiles.cs +++ b/Kavita.Models/AutoMapper/AutoMapperProfiles.cs @@ -1,49 +1,43 @@ using System; using System.Collections.Generic; using System.Linq; -using API.DTOs; -using API.DTOs.Account; -using API.DTOs.Annotations; -using API.DTOs.Collection; -using API.DTOs.Dashboard; -using API.DTOs.Device.EmailDevice; -using API.DTOs.Email; -using API.DTOs.Font; -using API.DTOs.KavitaPlus.Manage; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.MediaErrors; -using API.DTOs.Metadata; -using API.DTOs.Person; -using API.DTOs.Progress; -using API.DTOs.Reader; -using API.DTOs.ReadingLists; -using API.DTOs.Recommendation; -using API.DTOs.Scrobbling; -using API.DTOs.Search; -using API.DTOs.SeriesDetail; -using API.DTOs.Settings; -using API.DTOs.SideNav; -using API.DTOs.Stats; -using API.DTOs.Theme; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Entities.MetadataMatching; -using API.Entities.Person; -using API.Entities.Progress; -using API.Entities.Scrobble; -using API.Entities.User; -using API.Extensions.QueryExtensions.Filtering; -using API.Helpers; -using API.Helpers.Converters; using AutoMapper; -using EmailHistory = API.Entities.EmailHistory; -using MediaError = API.Entities.MediaError; -using PublicationStatus = API.Entities.Enums.PublicationStatus; -using SiteTheme = API.Entities.SiteTheme; +using Kavita.Common.Helpers; +using Kavita.Models.AutoMapper.Converters; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Account; +using Kavita.Models.DTOs.Annotations; +using Kavita.Models.DTOs.Collection; +using Kavita.Models.DTOs.Dashboard; +using Kavita.Models.DTOs.Device.EmailDevice; +using Kavita.Models.DTOs.Email; +using Kavita.Models.DTOs.Font; +using Kavita.Models.DTOs.KavitaPlus.Manage; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.MediaErrors; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.DTOs.Recommendation; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.DTOs.Search; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.DTOs.SideNav; +using Kavita.Models.DTOs.Stats; +using Kavita.Models.DTOs.Theme; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.MetadataMatching; +using Kavita.Models.Entities.Person; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.Scrobble; +using Kavita.Models.Entities.User; -namespace API.Data.AutoMapper; -#nullable enable +namespace Kavita.Models.AutoMapper; public class AutoMapperProfiles : Profile { @@ -283,7 +277,7 @@ public class AutoMapperProfiles : Profile CreateMap() .ForMember(dest => dest.BodyJustText, opt => - opt.MapFrom(src => ReviewHelper.GetCharacters(src.Body))); + opt.MapFrom(src => HtmlHelper.GetCharacters(src.Body))); CreateMap(); CreateMap() @@ -305,12 +299,6 @@ public class AutoMapperProfiles : Profile CreateMap() .ForMember(dest => dest.ToUserName, opt => opt.MapFrom(src => src.AppUser.UserName)); - CreateMap() - .ForMember(dest => dest.SeriesId, opt => opt.MapFrom(src => src.Volume.SeriesId)) - .ForMember(dest => dest.VolumeTitle, opt => opt.MapFrom(src => src.Volume.Name)) - .ForMember(dest => dest.LibraryId, opt => opt.MapFrom(src => src.Volume.Series.LibraryId)) - .ForMember(dest => dest.LibraryType, opt => opt.MapFrom(src => src.Volume.Series.Library.Type)); - CreateMap(); CreateMap() diff --git a/API/Data/AutoMapper/AutoMapperReadingListProfile.cs b/Kavita.Models/AutoMapper/AutoMapperReadingListProfile.cs similarity index 96% rename from API/Data/AutoMapper/AutoMapperReadingListProfile.cs rename to Kavita.Models/AutoMapper/AutoMapperReadingListProfile.cs index 9aeda5cb6..f318796b1 100644 --- a/API/Data/AutoMapper/AutoMapperReadingListProfile.cs +++ b/Kavita.Models/AutoMapper/AutoMapperReadingListProfile.cs @@ -1,10 +1,10 @@ using System; using System.Linq; -using API.DTOs.ReadingLists; -using API.Entities; using AutoMapper; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.Entities; -namespace API.Data.AutoMapper; +namespace Kavita.Models.AutoMapper; /// /// Maps ReadingList and ReadingListItem entities to DTOs with user progress attached at the DB level. diff --git a/API/Data/AutoMapper/AutoMapperSeriesProfile.cs b/Kavita.Models/AutoMapper/AutoMapperSeriesProfile.cs similarity index 94% rename from API/Data/AutoMapper/AutoMapperSeriesProfile.cs rename to Kavita.Models/AutoMapper/AutoMapperSeriesProfile.cs index 5825e2c82..d48bc5bfc 100644 --- a/API/Data/AutoMapper/AutoMapperSeriesProfile.cs +++ b/Kavita.Models/AutoMapper/AutoMapperSeriesProfile.cs @@ -1,10 +1,10 @@ using System; using System.Linq; -using API.DTOs; -using API.Entities; using AutoMapper; +using Kavita.Models.DTOs; +using Kavita.Models.Entities; -namespace API.Data.AutoMapper; +namespace Kavita.Models.AutoMapper; /// /// This is a way to attach progress at the DB level via a JOIN. Critical for healthy response time. diff --git a/API/Data/AutoMapper/AutoMapperVolumeProfile.cs b/Kavita.Models/AutoMapper/AutoMapperVolumeProfile.cs similarity index 91% rename from API/Data/AutoMapper/AutoMapperVolumeProfile.cs rename to Kavita.Models/AutoMapper/AutoMapperVolumeProfile.cs index 3c17e0acd..a6841d047 100644 --- a/API/Data/AutoMapper/AutoMapperVolumeProfile.cs +++ b/Kavita.Models/AutoMapper/AutoMapperVolumeProfile.cs @@ -1,9 +1,9 @@ using System.Linq; -using API.DTOs; -using API.Entities; using AutoMapper; +using Kavita.Models.DTOs; +using Kavita.Models.Entities; -namespace API.Data.AutoMapper; +namespace Kavita.Models.AutoMapper; /// /// Maps Volume entities to VolumeDto with user progress attached at the DB level via JOIN. diff --git a/API/Helpers/Converters/ServerSettingConverter.cs b/Kavita.Models/AutoMapper/Converters/ServerSettingConverter.cs similarity index 97% rename from API/Helpers/Converters/ServerSettingConverter.cs rename to Kavita.Models/AutoMapper/Converters/ServerSettingConverter.cs index 15a3d9c99..5a4f90faf 100644 --- a/API/Helpers/Converters/ServerSettingConverter.cs +++ b/Kavita.Models/AutoMapper/Converters/ServerSettingConverter.cs @@ -1,14 +1,13 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Text.Json; -using API.DTOs.Settings; -using API.Entities; -using API.Entities.Enums; using AutoMapper; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; -namespace API.Helpers.Converters; -#nullable enable +namespace Kavita.Models.AutoMapper.Converters; public class ServerSettingConverter : ITypeConverter, ServerSettingDto> { diff --git a/API/Helpers/Builders/AppUserBuilder.cs b/Kavita.Models/Builders/AppUserBuilder.cs similarity index 85% rename from API/Helpers/Builders/AppUserBuilder.cs rename to Kavita.Models/Builders/AppUserBuilder.cs index 25c3cef3f..c7db2947f 100644 --- a/API/Helpers/Builders/AppUserBuilder.cs +++ b/Kavita.Models/Builders/AppUserBuilder.cs @@ -1,14 +1,9 @@ -using System; using System.Linq; -using API.Data; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Enums.User; -using API.Entities.User; -using Kavita.Common; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; -namespace API.Helpers.Builders; -#nullable enable +namespace Kavita.Models.Builders; public class AppUserBuilder : IEntityBuilder { @@ -23,7 +18,7 @@ public class AppUserBuilder : IEntityBuilder Email = email, UserPreferences = new AppUserPreferences { - Theme = theme ?? Seed.DefaultThemes.First(), + Theme = theme ?? Defaults.DefaultThemes.First(), Locale = "en" }, ReadingLists = [], @@ -36,7 +31,7 @@ public class AppUserBuilder : IEntityBuilder DashboardStreams = [], SideNavStreams = [], ReadingProfiles = [], - AuthKeys = Seed.CreateDefaultAuthKeys() + AuthKeys = Defaults.CreateDefaultAuthKeys() }; } diff --git a/API/Helpers/Builders/AppUserChapterRatingBuilder.cs b/Kavita.Models/Builders/AppUserChapterRatingBuilder.cs similarity index 83% rename from API/Helpers/Builders/AppUserChapterRatingBuilder.cs rename to Kavita.Models/Builders/AppUserChapterRatingBuilder.cs index d524ed937..ef6ff5784 100644 --- a/API/Helpers/Builders/AppUserChapterRatingBuilder.cs +++ b/Kavita.Models/Builders/AppUserChapterRatingBuilder.cs @@ -1,11 +1,10 @@ #nullable enable using System; -using API.Entities; -using API.Entities.User; +using Kavita.Models.Entities.User; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; -public class ChapterRatingBuilder : IEntityBuilder +public class ChapterRatingBuilder : API.Helpers.Builders.IEntityBuilder { private readonly AppUserChapterRating _rating; public AppUserChapterRating Build() => _rating; diff --git a/API/Helpers/Builders/AppUserCollectionBuilder.cs b/Kavita.Models/Builders/AppUserCollectionBuilder.cs similarity index 91% rename from API/Helpers/Builders/AppUserCollectionBuilder.cs rename to Kavita.Models/Builders/AppUserCollectionBuilder.cs index e9bdcf977..113458dc4 100644 --- a/API/Helpers/Builders/AppUserCollectionBuilder.cs +++ b/Kavita.Models/Builders/AppUserCollectionBuilder.cs @@ -1,10 +1,10 @@ using System.Collections.Generic; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Services.Plus; +using Kavita.Common.Extensions; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class AppUserCollectionBuilder : IEntityBuilder { diff --git a/API/Helpers/Builders/AppUserReadingProfileBuilder.cs b/Kavita.Models/Builders/AppUserReadingProfileBuilder.cs similarity index 89% rename from API/Helpers/Builders/AppUserReadingProfileBuilder.cs rename to Kavita.Models/Builders/AppUserReadingProfileBuilder.cs index e44fcebc5..a0e04c9f4 100644 --- a/API/Helpers/Builders/AppUserReadingProfileBuilder.cs +++ b/Kavita.Models/Builders/AppUserReadingProfileBuilder.cs @@ -1,8 +1,9 @@ -using API.Entities; -using API.Entities.Enums; -using API.Extensions; +using Kavita.Common.Extensions; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class AppUserReadingProfileBuilder { diff --git a/API/Helpers/Builders/DeviceBuilder.cs b/Kavita.Models/Builders/DeviceBuilder.cs similarity index 83% rename from API/Helpers/Builders/DeviceBuilder.cs rename to Kavita.Models/Builders/DeviceBuilder.cs index 0eb3e6600..aa591cae6 100644 --- a/API/Helpers/Builders/DeviceBuilder.cs +++ b/Kavita.Models/Builders/DeviceBuilder.cs @@ -1,7 +1,7 @@ -using API.Entities; -using API.Entities.Enums.Device; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums.Device; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class DeviceBuilder : IEntityBuilder { diff --git a/API/Helpers/Builders/EntityBuilder.cs b/Kavita.Models/Builders/EntityBuilder.cs similarity index 100% rename from API/Helpers/Builders/EntityBuilder.cs rename to Kavita.Models/Builders/EntityBuilder.cs diff --git a/API/Helpers/Builders/ExternalSeriesMetadataBuilder.cs b/Kavita.Models/Builders/ExternalSeriesMetadataBuilder.cs similarity index 89% rename from API/Helpers/Builders/ExternalSeriesMetadataBuilder.cs rename to Kavita.Models/Builders/ExternalSeriesMetadataBuilder.cs index e716f5927..ae7976802 100644 --- a/API/Helpers/Builders/ExternalSeriesMetadataBuilder.cs +++ b/Kavita.Models/Builders/ExternalSeriesMetadataBuilder.cs @@ -1,7 +1,7 @@ using System; -using API.Entities.Metadata; +using Kavita.Models.Entities.Metadata; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class ExternalSeriesMetadataBuilder : IEntityBuilder { diff --git a/API/Helpers/Builders/FolderPathBuilder.cs b/Kavita.Models/Builders/FolderPathBuilder.cs similarity index 82% rename from API/Helpers/Builders/FolderPathBuilder.cs rename to Kavita.Models/Builders/FolderPathBuilder.cs index 07d6e2adf..698c6475a 100644 --- a/API/Helpers/Builders/FolderPathBuilder.cs +++ b/Kavita.Models/Builders/FolderPathBuilder.cs @@ -1,6 +1,6 @@ -using API.Entities; +using Kavita.Models.Entities; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class FolderPathBuilder : IEntityBuilder { diff --git a/API/Helpers/Builders/GenreBuilder.cs b/Kavita.Models/Builders/GenreBuilder.cs similarity index 80% rename from API/Helpers/Builders/GenreBuilder.cs rename to Kavita.Models/Builders/GenreBuilder.cs index e6265f078..a925edf50 100644 --- a/API/Helpers/Builders/GenreBuilder.cs +++ b/Kavita.Models/Builders/GenreBuilder.cs @@ -1,8 +1,8 @@ -using API.Entities; -using API.Entities.Metadata; -using API.Extensions; +using Kavita.Common.Extensions; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Metadata; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class GenreBuilder : IEntityBuilder { diff --git a/Kavita.Models/Builders/IEntityBuilder.cs b/Kavita.Models/Builders/IEntityBuilder.cs new file mode 100644 index 000000000..5c12ab777 --- /dev/null +++ b/Kavita.Models/Builders/IEntityBuilder.cs @@ -0,0 +1,6 @@ +namespace Kavita.Models.Builders; + +public interface IEntityBuilder +{ + public T Build(); +} diff --git a/API/Helpers/Builders/KoreaderBookDtoBuilder.cs b/Kavita.Models/Builders/KoreaderBookDtoBuilder.cs similarity index 95% rename from API/Helpers/Builders/KoreaderBookDtoBuilder.cs rename to Kavita.Models/Builders/KoreaderBookDtoBuilder.cs index 564f0ca33..a451ea1cd 100644 --- a/API/Helpers/Builders/KoreaderBookDtoBuilder.cs +++ b/Kavita.Models/Builders/KoreaderBookDtoBuilder.cs @@ -1,9 +1,9 @@ using System; using System.Security.Cryptography; using System.Text; -using API.DTOs.Koreader; +using Kavita.Models.DTOs.Koreader; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class KoreaderBookDtoBuilder : IEntityBuilder { diff --git a/API/Helpers/Builders/LibraryBuilder.cs b/Kavita.Models/Builders/LibraryBuilder.cs similarity index 95% rename from API/Helpers/Builders/LibraryBuilder.cs rename to Kavita.Models/Builders/LibraryBuilder.cs index 30181e37c..53d530c40 100644 --- a/API/Helpers/Builders/LibraryBuilder.cs +++ b/Kavita.Models/Builders/LibraryBuilder.cs @@ -1,9 +1,10 @@ using System.Collections.Generic; using System.Linq; -using API.Entities; -using API.Entities.Enums; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class LibraryBuilder : IEntityBuilder { diff --git a/Kavita.Models/Builders/MediaErrorBuilder.cs b/Kavita.Models/Builders/MediaErrorBuilder.cs new file mode 100644 index 000000000..b5e91d3d9 --- /dev/null +++ b/Kavita.Models/Builders/MediaErrorBuilder.cs @@ -0,0 +1,28 @@ +using System.IO; +using Kavita.Common.Extensions; +using Kavita.Models.Entities; + +namespace Kavita.Models.Builders; + +public class MediaErrorBuilder(string filePath): IEntityBuilder +{ + private readonly MediaError _mediaError = new() + { + FilePath = filePath.ToNormalized(), + Extension = Path.GetExtension(filePath).Replace(".", string.Empty).ToUpperInvariant() + }; + + public MediaError Build() => _mediaError; + + public MediaErrorBuilder WithComment(string comment) + { + _mediaError.Comment = comment.Trim(); + return this; + } + + public MediaErrorBuilder WithDetails(string details) + { + _mediaError.Details = details.Trim(); + return this; + } +} diff --git a/API/Helpers/Builders/PersonAliasBuilder.cs b/Kavita.Models/Builders/PersonAliasBuilder.cs similarity index 76% rename from API/Helpers/Builders/PersonAliasBuilder.cs rename to Kavita.Models/Builders/PersonAliasBuilder.cs index e54ea8975..6d4ed8d64 100644 --- a/API/Helpers/Builders/PersonAliasBuilder.cs +++ b/Kavita.Models/Builders/PersonAliasBuilder.cs @@ -1,7 +1,7 @@ -using API.Entities.Person; -using API.Extensions; +using Kavita.Common.Extensions; +using Kavita.Models.Entities.Person; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class PersonAliasBuilder : IEntityBuilder { diff --git a/API/Helpers/Builders/PersonBuilder.cs b/Kavita.Models/Builders/PersonBuilder.cs similarity index 92% rename from API/Helpers/Builders/PersonBuilder.cs rename to Kavita.Models/Builders/PersonBuilder.cs index 3bf33e7a9..37eb49b35 100644 --- a/API/Helpers/Builders/PersonBuilder.cs +++ b/Kavita.Models/Builders/PersonBuilder.cs @@ -1,10 +1,10 @@ #nullable enable using System.Collections.Generic; using System.Linq; -using API.Entities.Person; -using API.Extensions; +using Kavita.Common.Extensions; +using Kavita.Models.Entities.Person; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class PersonBuilder : IEntityBuilder { diff --git a/API/Helpers/Builders/RatingBuilder.cs b/Kavita.Models/Builders/RatingBuilder.cs similarity index 90% rename from API/Helpers/Builders/RatingBuilder.cs rename to Kavita.Models/Builders/RatingBuilder.cs index c0dc9e57a..fade4760e 100644 --- a/API/Helpers/Builders/RatingBuilder.cs +++ b/Kavita.Models/Builders/RatingBuilder.cs @@ -1,8 +1,8 @@ #nullable enable using System; -using API.Entities; +using Kavita.Models.Entities.User; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class RatingBuilder : IEntityBuilder { @@ -25,7 +25,7 @@ public class RatingBuilder : IEntityBuilder _rating.Rating = Math.Clamp(rating, 0, 5); return this; } - + public RatingBuilder WithBody(string body) { diff --git a/API/Helpers/Builders/ReadingListBuilder.cs b/Kavita.Models/Builders/ReadingListBuilder.cs similarity index 91% rename from API/Helpers/Builders/ReadingListBuilder.cs rename to Kavita.Models/Builders/ReadingListBuilder.cs index e05a92096..bcd057d67 100644 --- a/API/Helpers/Builders/ReadingListBuilder.cs +++ b/Kavita.Models/Builders/ReadingListBuilder.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; +using Kavita.Common.Extensions; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class ReadingListBuilder : IEntityBuilder { diff --git a/API/Helpers/Builders/ReadingListItemBuilder.cs b/Kavita.Models/Builders/ReadingListItemBuilder.cs similarity index 87% rename from API/Helpers/Builders/ReadingListItemBuilder.cs rename to Kavita.Models/Builders/ReadingListItemBuilder.cs index 86ca4cfc8..5ef621353 100644 --- a/API/Helpers/Builders/ReadingListItemBuilder.cs +++ b/Kavita.Models/Builders/ReadingListItemBuilder.cs @@ -1,6 +1,6 @@ -using API.Entities; +using Kavita.Models.Entities; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class ReadingListItemBuilder : IEntityBuilder { diff --git a/API/Helpers/Builders/ScrobbleHoldBuilder.cs b/Kavita.Models/Builders/ScrobbleHoldBuilder.cs similarity index 86% rename from API/Helpers/Builders/ScrobbleHoldBuilder.cs rename to Kavita.Models/Builders/ScrobbleHoldBuilder.cs index cd03a08f0..4d5674e9f 100644 --- a/API/Helpers/Builders/ScrobbleHoldBuilder.cs +++ b/Kavita.Models/Builders/ScrobbleHoldBuilder.cs @@ -1,7 +1,6 @@ -using API.Entities.Scrobble; +using Kavita.Models.Entities.Scrobble; -namespace API.Helpers.Builders; -#nullable enable +namespace Kavita.Models.Builders; public class ScrobbleHoldBuilder : IEntityBuilder { diff --git a/API/Helpers/Builders/SeriesBuilder.cs b/Kavita.Models/Builders/SeriesBuilder.cs similarity index 93% rename from API/Helpers/Builders/SeriesBuilder.cs rename to Kavita.Models/Builders/SeriesBuilder.cs index 96e820659..6bced9f8d 100644 --- a/API/Helpers/Builders/SeriesBuilder.cs +++ b/Kavita.Models/Builders/SeriesBuilder.cs @@ -1,13 +1,13 @@ using System.Collections.Generic; using System.Linq; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Extensions; +using Kavita.Common.Extensions; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; -public class SeriesBuilder : IEntityBuilder +public class SeriesBuilder : API.Helpers.Builders.IEntityBuilder { private readonly Series _series; public Series Build() diff --git a/API/Helpers/Builders/SeriesMetadataBuilder.cs b/Kavita.Models/Builders/SeriesMetadataBuilder.cs similarity index 95% rename from API/Helpers/Builders/SeriesMetadataBuilder.cs rename to Kavita.Models/Builders/SeriesMetadataBuilder.cs index 462bc4455..d5a121682 100644 --- a/API/Helpers/Builders/SeriesMetadataBuilder.cs +++ b/Kavita.Models/Builders/SeriesMetadataBuilder.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Entities.Person; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.Person; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class SeriesMetadataBuilder : IEntityBuilder { diff --git a/API/Helpers/Builders/TagBuilder.cs b/Kavita.Models/Builders/TagBuilder.cs similarity index 82% rename from API/Helpers/Builders/TagBuilder.cs rename to Kavita.Models/Builders/TagBuilder.cs index 623587fd1..d1ebd4dfb 100644 --- a/API/Helpers/Builders/TagBuilder.cs +++ b/Kavita.Models/Builders/TagBuilder.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; -using API.Entities; -using API.Entities.Metadata; -using API.Extensions; +using Kavita.Common.Extensions; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Metadata; -namespace API.Helpers.Builders; +namespace Kavita.Models.Builders; public class TagBuilder : IEntityBuilder { diff --git a/API/Constants/CacheProfiles.cs b/Kavita.Models/Constants/CacheProfiles.cs similarity index 96% rename from API/Constants/CacheProfiles.cs rename to Kavita.Models/Constants/CacheProfiles.cs index afc82f19c..98f99f0a6 100644 --- a/API/Constants/CacheProfiles.cs +++ b/Kavita.Models/Constants/CacheProfiles.cs @@ -1,4 +1,4 @@ -namespace API.Constants; +namespace Kavita.Models.Constants; public static class EasyCacheProfiles { diff --git a/API/Constants/ControllerConstants.cs b/Kavita.Models/Constants/ControllerConstants.cs similarity index 72% rename from API/Constants/ControllerConstants.cs rename to Kavita.Models/Constants/ControllerConstants.cs index 34a2482ee..0d53d04dd 100644 --- a/API/Constants/ControllerConstants.cs +++ b/Kavita.Models/Constants/ControllerConstants.cs @@ -1,4 +1,4 @@ -namespace API.Constants; +namespace Kavita.Models.Constants; public abstract class ControllerConstants { diff --git a/Kavita.Models/Constants/ParserConstants.cs b/Kavita.Models/Constants/ParserConstants.cs new file mode 100644 index 000000000..6463db2bd --- /dev/null +++ b/Kavita.Models/Constants/ParserConstants.cs @@ -0,0 +1,14 @@ +namespace Kavita.Models.Constants; + +public static class ParserConstants +{ + public const string DefaultChapter = "-100000"; + public const string LooseLeafVolume = "-100000"; + public const int DefaultChapterNumber = -100_000; + public const int LooseLeafVolumeNumber = -100_000; + /// + /// The Volume Number of Specials to reside in + /// + public const int SpecialVolumeNumber = 100_000; + public const string SpecialVolume = "100000"; +} diff --git a/API/Constants/PolicyConstants.cs b/Kavita.Models/Constants/PolicyConstants.cs similarity index 98% rename from API/Constants/PolicyConstants.cs rename to Kavita.Models/Constants/PolicyConstants.cs index 734885806..618cd47f5 100644 --- a/API/Constants/PolicyConstants.cs +++ b/Kavita.Models/Constants/PolicyConstants.cs @@ -1,6 +1,6 @@ using System.Collections.Immutable; -namespace API.Constants; +namespace Kavita.Models.Constants; /// /// Role-based Security diff --git a/API/Constants/PolicyGroups.cs b/Kavita.Models/Constants/PolicyGroups.cs similarity index 74% rename from API/Constants/PolicyGroups.cs rename to Kavita.Models/Constants/PolicyGroups.cs index 47f65f798..27d6e0cde 100644 --- a/API/Constants/PolicyGroups.cs +++ b/Kavita.Models/Constants/PolicyGroups.cs @@ -1,4 +1,4 @@ -namespace API.Constants; +namespace Kavita.Models.Constants; /// /// Constants for Higher level policy roles @@ -17,4 +17,8 @@ public static class PolicyGroups /// Requires Admin or Change Password to execute /// public const string ChangePasswordPolicy = "RequireChangePasswordRole"; + /// + /// Requires Admin or Bookmark to execute + /// + public const string BookmarkPolicy = "RequireBookmarkRole"; } diff --git a/API/Constants/ResponseCacheProfiles.cs b/Kavita.Models/Constants/ResponseCacheProfiles.cs similarity index 93% rename from API/Constants/ResponseCacheProfiles.cs rename to Kavita.Models/Constants/ResponseCacheProfiles.cs index c68b49f62..f85ad7567 100644 --- a/API/Constants/ResponseCacheProfiles.cs +++ b/Kavita.Models/Constants/ResponseCacheProfiles.cs @@ -1,4 +1,4 @@ -namespace API.Constants; +namespace Kavita.Models.Constants; public static class ResponseCacheProfiles { diff --git a/Kavita.Models/Constants/TaskSchedulerConstants.cs b/Kavita.Models/Constants/TaskSchedulerConstants.cs new file mode 100644 index 000000000..af26b84e2 --- /dev/null +++ b/Kavita.Models/Constants/TaskSchedulerConstants.cs @@ -0,0 +1,26 @@ +namespace Kavita.Models.Constants; + +public static class TaskSchedulerConstants +{ + public const string ScanQueue = "scan"; + public const string DefaultQueue = "default"; + public const string RemoveFromWantToReadTaskId = "remove-from-want-to-read"; + public const string UpdateYearlyStatsTaskId = "update-yearly-stats"; + public const string SyncThemesTaskId = "sync-themes"; + public const string CheckForUpdateId = "check-updates"; + public const string CleanupDbTaskId = "cleanup-db"; + public const string CleanupTaskId = "cleanup"; + public const string BackupTaskId = "backup"; + public const string ScanLibrariesTaskId = "scan-libraries"; + public const string ReportStatsTaskId = "report-stats"; + 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"; + public const string KavitaPlusStackSyncId = "kavita+-stack-sync"; + public const string KavitaPlusWantToReadSyncId = "kavita+-want-to-read-sync"; + public const string ReadingHistoryAggregationId = "reading-history-aggregation"; + public const string AuthKeyExpirationId = "auth-key-expiration"; + public const string EnsureSideNavId = "ensure-sidenav"; +} diff --git a/API/DTOs/Account/AgeRestrictionDto.cs b/Kavita.Models/DTOs/Account/AgeRestrictionDto.cs similarity index 87% rename from API/DTOs/Account/AgeRestrictionDto.cs rename to Kavita.Models/DTOs/Account/AgeRestrictionDto.cs index 6505bdbff..06f7431af 100644 --- a/API/DTOs/Account/AgeRestrictionDto.cs +++ b/Kavita.Models/DTOs/Account/AgeRestrictionDto.cs @@ -1,6 +1,8 @@ -using API.Entities.Enums; + -namespace API.DTOs.Account; +using Kavita.Models.Entities.Enums; + +namespace Kavita.Models.DTOs.Account; public sealed record AgeRestrictionDto { diff --git a/API/DTOs/Account/AuthKeyDto.cs b/Kavita.Models/DTOs/Account/AuthKeyDto.cs similarity index 88% rename from API/DTOs/Account/AuthKeyDto.cs rename to Kavita.Models/DTOs/Account/AuthKeyDto.cs index 4f2625cbf..38e18623d 100644 --- a/API/DTOs/Account/AuthKeyDto.cs +++ b/Kavita.Models/DTOs/Account/AuthKeyDto.cs @@ -1,7 +1,8 @@ -using System; -using API.Entities.Enums.User; + +using System; +using Kavita.Models.Entities.Enums.User; -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; public sealed record AuthKeyDto { diff --git a/Kavita.Models/DTOs/Account/AuthKeyExpiresAtDto.cs b/Kavita.Models/DTOs/Account/AuthKeyExpiresAtDto.cs new file mode 100644 index 000000000..7e8b52937 --- /dev/null +++ b/Kavita.Models/DTOs/Account/AuthKeyExpiresAtDto.cs @@ -0,0 +1,8 @@ +using System; + +namespace Kavita.Models.DTOs.Account; + +public sealed record AuthKeyExpiresAtDto +{ + public required DateTime? ExpiresAt { get; set; } +} diff --git a/API/DTOs/Account/ConfirmEmailDto.cs b/Kavita.Models/DTOs/Account/ConfirmEmailDto.cs similarity index 91% rename from API/DTOs/Account/ConfirmEmailDto.cs rename to Kavita.Models/DTOs/Account/ConfirmEmailDto.cs index 413f9f34a..80f9a3e4c 100644 --- a/API/DTOs/Account/ConfirmEmailDto.cs +++ b/Kavita.Models/DTOs/Account/ConfirmEmailDto.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; public sealed record ConfirmEmailDto { diff --git a/API/DTOs/Account/ConfirmEmailUpdateDto.cs b/Kavita.Models/DTOs/Account/ConfirmEmailUpdateDto.cs similarity index 85% rename from API/DTOs/Account/ConfirmEmailUpdateDto.cs rename to Kavita.Models/DTOs/Account/ConfirmEmailUpdateDto.cs index 2a0738e35..0ed1e4901 100644 --- a/API/DTOs/Account/ConfirmEmailUpdateDto.cs +++ b/Kavita.Models/DTOs/Account/ConfirmEmailUpdateDto.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; public sealed record ConfirmEmailUpdateDto { diff --git a/API/DTOs/Account/ConfirmMigrationEmailDto.cs b/Kavita.Models/DTOs/Account/ConfirmMigrationEmailDto.cs similarity index 78% rename from API/DTOs/Account/ConfirmMigrationEmailDto.cs rename to Kavita.Models/DTOs/Account/ConfirmMigrationEmailDto.cs index cdfc1505c..a088fefcd 100644 --- a/API/DTOs/Account/ConfirmMigrationEmailDto.cs +++ b/Kavita.Models/DTOs/Account/ConfirmMigrationEmailDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; public sealed record ConfirmMigrationEmailDto { diff --git a/API/DTOs/Account/ConfirmPasswordResetDto.cs b/Kavita.Models/DTOs/Account/ConfirmPasswordResetDto.cs similarity index 89% rename from API/DTOs/Account/ConfirmPasswordResetDto.cs rename to Kavita.Models/DTOs/Account/ConfirmPasswordResetDto.cs index 00aff301b..5bd43aeb7 100644 --- a/API/DTOs/Account/ConfirmPasswordResetDto.cs +++ b/Kavita.Models/DTOs/Account/ConfirmPasswordResetDto.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; public sealed record ConfirmPasswordResetDto { diff --git a/API/DTOs/Account/InviteUserDto.cs b/Kavita.Models/DTOs/Account/InviteUserDto.cs similarity index 95% rename from API/DTOs/Account/InviteUserDto.cs rename to Kavita.Models/DTOs/Account/InviteUserDto.cs index c12bebc2b..1199b5cba 100644 --- a/API/DTOs/Account/InviteUserDto.cs +++ b/Kavita.Models/DTOs/Account/InviteUserDto.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; public sealed record InviteUserDto { diff --git a/API/DTOs/Account/InviteUserResponse.cs b/Kavita.Models/DTOs/Account/InviteUserResponse.cs similarity index 92% rename from API/DTOs/Account/InviteUserResponse.cs rename to Kavita.Models/DTOs/Account/InviteUserResponse.cs index ed16bd05e..dbba7aafc 100644 --- a/API/DTOs/Account/InviteUserResponse.cs +++ b/Kavita.Models/DTOs/Account/InviteUserResponse.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; public sealed record InviteUserResponse { diff --git a/API/DTOs/Account/LoginDto.cs b/Kavita.Models/DTOs/Account/LoginDto.cs similarity index 88% rename from API/DTOs/Account/LoginDto.cs rename to Kavita.Models/DTOs/Account/LoginDto.cs index 97338640b..51f3065f8 100644 --- a/API/DTOs/Account/LoginDto.cs +++ b/Kavita.Models/DTOs/Account/LoginDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; #nullable enable public sealed record LoginDto diff --git a/API/DTOs/Account/MemberDto.cs b/Kavita.Models/DTOs/Account/MemberDto.cs similarity index 91% rename from API/DTOs/Account/MemberDto.cs rename to Kavita.Models/DTOs/Account/MemberDto.cs index 4f0081c4f..c0aab2cc5 100644 --- a/API/DTOs/Account/MemberDto.cs +++ b/Kavita.Models/DTOs/Account/MemberDto.cs @@ -1,9 +1,8 @@ using System; using System.Collections.Generic; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Account; -#nullable enable +namespace Kavita.Models.DTOs.Account; /// /// Represents a member of a Kavita server. diff --git a/API/DTOs/Account/MemberInfoDto.cs b/Kavita.Models/DTOs/Account/MemberInfoDto.cs similarity index 80% rename from API/DTOs/Account/MemberInfoDto.cs rename to Kavita.Models/DTOs/Account/MemberInfoDto.cs index 0d3f4954e..56e2f0073 100644 --- a/API/DTOs/Account/MemberInfoDto.cs +++ b/Kavita.Models/DTOs/Account/MemberInfoDto.cs @@ -1,7 +1,7 @@ -using System; + +using System; -namespace API.DTOs.Account; -#nullable enable +namespace Kavita.Models.DTOs.Account; public sealed record MemberInfoDto { diff --git a/API/DTOs/Account/MigrateUserEmailDto.cs b/Kavita.Models/DTOs/Account/MigrateUserEmailDto.cs similarity index 83% rename from API/DTOs/Account/MigrateUserEmailDto.cs rename to Kavita.Models/DTOs/Account/MigrateUserEmailDto.cs index 4630c510f..2b7264d59 100644 --- a/API/DTOs/Account/MigrateUserEmailDto.cs +++ b/Kavita.Models/DTOs/Account/MigrateUserEmailDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; public sealed record MigrateUserEmailDto { diff --git a/API/DTOs/Account/ResetPasswordDto.cs b/Kavita.Models/DTOs/Account/ResetPasswordDto.cs similarity index 94% rename from API/DTOs/Account/ResetPasswordDto.cs rename to Kavita.Models/DTOs/Account/ResetPasswordDto.cs index 545ca5ba6..c6c4449b0 100644 --- a/API/DTOs/Account/ResetPasswordDto.cs +++ b/Kavita.Models/DTOs/Account/ResetPasswordDto.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; public sealed record ResetPasswordDto { diff --git a/API/DTOs/Account/RotateAuthKeyRequestDto.cs b/Kavita.Models/DTOs/Account/RotateAuthKeyRequestDto.cs similarity index 89% rename from API/DTOs/Account/RotateAuthKeyRequestDto.cs rename to Kavita.Models/DTOs/Account/RotateAuthKeyRequestDto.cs index ac7781dba..e654c3640 100644 --- a/API/DTOs/Account/RotateAuthKeyRequestDto.cs +++ b/Kavita.Models/DTOs/Account/RotateAuthKeyRequestDto.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; #nullable enable public sealed record RotateAuthKeyRequestDto diff --git a/API/DTOs/Account/TokenRequestDto.cs b/Kavita.Models/DTOs/Account/TokenRequestDto.cs similarity index 78% rename from API/DTOs/Account/TokenRequestDto.cs rename to Kavita.Models/DTOs/Account/TokenRequestDto.cs index 5c798721c..0f505d6bb 100644 --- a/API/DTOs/Account/TokenRequestDto.cs +++ b/Kavita.Models/DTOs/Account/TokenRequestDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; public sealed record TokenRequestDto { diff --git a/API/DTOs/Account/UpdateAgeRestrictionDto.cs b/Kavita.Models/DTOs/Account/UpdateAgeRestrictionDto.cs similarity index 74% rename from API/DTOs/Account/UpdateAgeRestrictionDto.cs rename to Kavita.Models/DTOs/Account/UpdateAgeRestrictionDto.cs index 2fa9c89d2..435a461c3 100644 --- a/API/DTOs/Account/UpdateAgeRestrictionDto.cs +++ b/Kavita.Models/DTOs/Account/UpdateAgeRestrictionDto.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; public sealed record UpdateAgeRestrictionDto { diff --git a/API/DTOs/Account/UpdateEmailDto.cs b/Kavita.Models/DTOs/Account/UpdateEmailDto.cs similarity index 77% rename from API/DTOs/Account/UpdateEmailDto.cs rename to Kavita.Models/DTOs/Account/UpdateEmailDto.cs index 873862ba1..fdd5b4f36 100644 --- a/API/DTOs/Account/UpdateEmailDto.cs +++ b/Kavita.Models/DTOs/Account/UpdateEmailDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Account; +namespace Kavita.Models.DTOs.Account; public sealed record UpdateEmailDto { diff --git a/API/DTOs/Account/UpdateUserDto.cs b/Kavita.Models/DTOs/Account/UpdateUserDto.cs similarity index 77% rename from API/DTOs/Account/UpdateUserDto.cs rename to Kavita.Models/DTOs/Account/UpdateUserDto.cs index 1fb780d6d..5b52ea2f3 100644 --- a/API/DTOs/Account/UpdateUserDto.cs +++ b/Kavita.Models/DTOs/Account/UpdateUserDto.cs @@ -1,14 +1,15 @@ using System.Collections.Generic; -using API.Entities.Enums; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; -namespace API.DTOs.Account; -#nullable enable +namespace Kavita.Models.DTOs.Account; public sealed record UpdateUserDto { - /// + /// public int UserId { get; set; } - /// + /// public string Username { get; set; } = default!; /// /// List of Roles to assign to user. If admin not present, Pleb will be applied. @@ -23,7 +24,7 @@ public sealed record UpdateUserDto /// An Age Rating which will limit the account to seeing everything equal to or below said rating. /// public AgeRestrictionDto AgeRestriction { get; init; } = default!; - /// + /// public string? Email { get; set; } = default!; public IdentityProvider IdentityProvider { get; init; } = IdentityProvider.Kavita; } diff --git a/API/DTOs/Annotations/FullAnnotationDto.cs b/Kavita.Models/DTOs/Annotations/FullAnnotationDto.cs similarity index 95% rename from API/DTOs/Annotations/FullAnnotationDto.cs rename to Kavita.Models/DTOs/Annotations/FullAnnotationDto.cs index 17dbe0579..a395bfa26 100644 --- a/API/DTOs/Annotations/FullAnnotationDto.cs +++ b/Kavita.Models/DTOs/Annotations/FullAnnotationDto.cs @@ -1,7 +1,7 @@ using System; using System.Text.Json.Serialization; -namespace API.DTOs.Annotations; +namespace Kavita.Models.DTOs.Annotations; public sealed record FullAnnotationDto { diff --git a/API/DTOs/Archive/ArchiveLibrary.cs b/Kavita.Models/DTOs/Archive/ArchiveLibrary.cs similarity index 91% rename from API/DTOs/Archive/ArchiveLibrary.cs rename to Kavita.Models/DTOs/Archive/ArchiveLibrary.cs index a3beae9bc..5330f84a5 100644 --- a/API/DTOs/Archive/ArchiveLibrary.cs +++ b/Kavita.Models/DTOs/Archive/ArchiveLibrary.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Archive; +namespace Kavita.Models.DTOs.Archive; /// /// Represents which library should handle opening this library diff --git a/API/DTOs/BulkActionDto.cs b/Kavita.Models/DTOs/BulkActionDto.cs similarity index 88% rename from API/DTOs/BulkActionDto.cs rename to Kavita.Models/DTOs/BulkActionDto.cs index c26a73e9c..056813395 100644 --- a/API/DTOs/BulkActionDto.cs +++ b/Kavita.Models/DTOs/BulkActionDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record BulkActionDto { diff --git a/API/DTOs/ChapterDetailPlusDto.cs b/Kavita.Models/DTOs/ChapterDetailPlusDto.cs similarity index 81% rename from API/DTOs/ChapterDetailPlusDto.cs rename to Kavita.Models/DTOs/ChapterDetailPlusDto.cs index d99482e55..f9636aa8b 100644 --- a/API/DTOs/ChapterDetailPlusDto.cs +++ b/Kavita.Models/DTOs/ChapterDetailPlusDto.cs @@ -1,8 +1,8 @@ #nullable enable using System.Collections.Generic; -using API.DTOs.SeriesDetail; +using Kavita.Models.DTOs.SeriesDetail; -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record ChapterDetailPlusDto { diff --git a/API/DTOs/ChapterDto.cs b/Kavita.Models/DTOs/ChapterDto.cs similarity index 64% rename from API/DTOs/ChapterDto.cs rename to Kavita.Models/DTOs/ChapterDto.cs index 98ef6c01e..e935fcd60 100644 --- a/API/DTOs/ChapterDto.cs +++ b/Kavita.Models/DTOs/ChapterDto.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; -using API.DTOs.Metadata; -using API.DTOs.Person; -using API.Entities.Enums; -using API.Entities.Interfaces; +using System.Runtime.InteropServices.JavaScript; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Person; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable /// @@ -15,24 +17,24 @@ namespace API.DTOs; /// public class ChapterDto : IHasReadTimeEstimate, IHasCoverImage { - /// + /// public int Id { get; init; } - /// + /// public string Range { get; init; } = default!; - /// + /// [Obsolete("Use MinNumber and MaxNumber instead")] public string Number { get; init; } = default!; - /// + /// public float MinNumber { get; init; } - /// + /// public float MaxNumber { get; init; } - /// + /// public float SortOrder { get; set; } - /// + /// public int Pages { get; init; } - /// + /// public bool IsSpecial { get; init; } - /// + /// public string Title { get; set; } = default!; /// /// The files that represent this Chapter @@ -55,25 +57,25 @@ public class ChapterDto : IHasReadTimeEstimate, IHasCoverImage /// The last time a chapter was read by current authenticated user /// public DateTime LastReadingProgress { get; set; } - /// + /// public bool CoverImageLocked { get; set; } - /// + /// public int VolumeId { get; init; } - /// + /// public DateTime CreatedUtc { get; set; } - /// + /// public DateTime LastModifiedUtc { get; set; } - /// + /// public DateTime Created { get; set; } - /// + /// public DateTime ReleaseDate { get; init; } - /// + /// public string TitleName { get; set; } = default!; - /// + /// public string Summary { get; init; } = default!; - /// + /// public AgeRating AgeRating { get; init; } - /// + /// public long WordCount { get; set; } = 0L; /// /// Formatted Volume title ie) Volume 2. @@ -86,9 +88,9 @@ public class ChapterDto : IHasReadTimeEstimate, IHasCoverImage public int MaxHoursToRead { get; set; } /// public float AvgHoursToRead { get; set; } - /// + /// public string WebLinks { get; set; } - /// + /// public string ISBN { get; set; } #region Metadata @@ -114,64 +116,64 @@ public class ChapterDto : IHasReadTimeEstimate, IHasCoverImage /// public ICollection Tags { get; set; } = new List(); public PublicationStatus PublicationStatus { get; set; } - /// + /// public string? Language { get; set; } - /// + /// public int Count { get; set; } - /// + /// public int TotalCount { get; set; } - /// + /// public bool LanguageLocked { get; set; } - /// + /// public bool SummaryLocked { get; set; } - /// + /// public bool AgeRatingLocked { get; set; } public bool PublicationStatusLocked { get; set; } - /// + /// public bool GenresLocked { get; set; } - /// + /// public bool TagsLocked { get; set; } - /// + /// public bool WriterLocked { get; set; } - /// + /// public bool CharacterLocked { get; set; } - /// + /// public bool ColoristLocked { get; set; } - /// + /// public bool EditorLocked { get; set; } - /// + /// public bool InkerLocked { get; set; } - /// + /// public bool ImprintLocked { get; set; } - /// + /// public bool LettererLocked { get; set; } - /// + /// public bool PencillerLocked { get; set; } - /// + /// public bool PublisherLocked { get; set; } - /// + /// public bool TranslatorLocked { get; set; } - /// + /// public bool TeamLocked { get; set; } - /// + /// public bool LocationLocked { get; set; } - /// + /// public bool CoverArtistLocked { get; set; } - /// + /// public bool ReleaseDateLocked { get; set; } - /// + /// public bool TitleNameLocked { get; set; } - /// + /// public bool SortOrderLocked { get; set; } #endregion - /// + /// public string? CoverImage { get; set; } - /// + /// public string? PrimaryColor { get; set; } = string.Empty; - /// + /// public string? SecondaryColor { get; set; } = string.Empty; public MangaFormat? Format => Files.FirstOrDefault()?.Format; diff --git a/API/DTOs/CheckForFilesInFolderRootsDto.cs b/Kavita.Models/DTOs/CheckForFilesInFolderRootsDto.cs similarity index 82% rename from API/DTOs/CheckForFilesInFolderRootsDto.cs rename to Kavita.Models/DTOs/CheckForFilesInFolderRootsDto.cs index 42d4e2747..a3e0d0d3b 100644 --- a/API/DTOs/CheckForFilesInFolderRootsDto.cs +++ b/Kavita.Models/DTOs/CheckForFilesInFolderRootsDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record CheckForFilesInFolderRootsDto { diff --git a/API/DTOs/Collection/AppUserCollectionDto.cs b/Kavita.Models/DTOs/Collection/AppUserCollectionDto.cs similarity index 94% rename from API/DTOs/Collection/AppUserCollectionDto.cs rename to Kavita.Models/DTOs/Collection/AppUserCollectionDto.cs index 0634b5d83..d51285dab 100644 --- a/API/DTOs/Collection/AppUserCollectionDto.cs +++ b/Kavita.Models/DTOs/Collection/AppUserCollectionDto.cs @@ -1,9 +1,8 @@ using System; -using API.Entities.Enums; -using API.Entities.Interfaces; -using API.Services.Plus; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; -namespace API.DTOs.Collection; +namespace Kavita.Models.DTOs.Collection; #nullable enable public sealed record AppUserCollectionDto : IHasCoverImage diff --git a/API/DTOs/Collection/DeleteCollectionsDto.cs b/Kavita.Models/DTOs/Collection/DeleteCollectionsDto.cs similarity index 82% rename from API/DTOs/Collection/DeleteCollectionsDto.cs rename to Kavita.Models/DTOs/Collection/DeleteCollectionsDto.cs index c0b94e9a1..8322c0f9b 100644 --- a/API/DTOs/Collection/DeleteCollectionsDto.cs +++ b/Kavita.Models/DTOs/Collection/DeleteCollectionsDto.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace API.DTOs.Collection; +namespace Kavita.Models.DTOs.Collection; public class DeleteCollectionsDto { diff --git a/API/DTOs/Collection/MalStackDto.cs b/Kavita.Models/DTOs/Collection/MalStackDto.cs similarity index 93% rename from API/DTOs/Collection/MalStackDto.cs rename to Kavita.Models/DTOs/Collection/MalStackDto.cs index d9d902e88..d8160673e 100644 --- a/API/DTOs/Collection/MalStackDto.cs +++ b/Kavita.Models/DTOs/Collection/MalStackDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Collection; +namespace Kavita.Models.DTOs.Collection; #nullable enable /// diff --git a/API/DTOs/Collection/PromoteCollectionsDto.cs b/Kavita.Models/DTOs/Collection/PromoteCollectionsDto.cs similarity index 80% rename from API/DTOs/Collection/PromoteCollectionsDto.cs rename to Kavita.Models/DTOs/Collection/PromoteCollectionsDto.cs index 2e2ab793b..e94c6bf4e 100644 --- a/API/DTOs/Collection/PromoteCollectionsDto.cs +++ b/Kavita.Models/DTOs/Collection/PromoteCollectionsDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Collection; +namespace Kavita.Models.DTOs.Collection; public class PromoteCollectionsDto { diff --git a/API/DTOs/CollectionTags/CollectionTagBulkAddDto.cs b/Kavita.Models/DTOs/CollectionTags/CollectionTagBulkAddDto.cs similarity index 91% rename from API/DTOs/CollectionTags/CollectionTagBulkAddDto.cs rename to Kavita.Models/DTOs/CollectionTags/CollectionTagBulkAddDto.cs index 0a2270fbf..0e5e41949 100644 --- a/API/DTOs/CollectionTags/CollectionTagBulkAddDto.cs +++ b/Kavita.Models/DTOs/CollectionTags/CollectionTagBulkAddDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.CollectionTags; +namespace Kavita.Models.DTOs.CollectionTags; public sealed record CollectionTagBulkAddDto { diff --git a/API/DTOs/CollectionTags/CollectionTagDto.cs b/Kavita.Models/DTOs/CollectionTags/CollectionTagDto.cs similarity index 95% rename from API/DTOs/CollectionTags/CollectionTagDto.cs rename to Kavita.Models/DTOs/CollectionTags/CollectionTagDto.cs index 911622051..d5524838c 100644 --- a/API/DTOs/CollectionTags/CollectionTagDto.cs +++ b/Kavita.Models/DTOs/CollectionTags/CollectionTagDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.CollectionTags; +namespace Kavita.Models.DTOs.CollectionTags; [Obsolete("Use AppUserCollectionDto")] public sealed record CollectionTagDto diff --git a/API/DTOs/CollectionTags/UpdateSeriesForTagDto.cs b/Kavita.Models/DTOs/CollectionTags/UpdateSeriesForTagDto.cs similarity index 73% rename from API/DTOs/CollectionTags/UpdateSeriesForTagDto.cs rename to Kavita.Models/DTOs/CollectionTags/UpdateSeriesForTagDto.cs index becc7034f..ee3fb1d2d 100644 --- a/API/DTOs/CollectionTags/UpdateSeriesForTagDto.cs +++ b/Kavita.Models/DTOs/CollectionTags/UpdateSeriesForTagDto.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using API.DTOs.Collection; +using Kavita.Models.DTOs.Collection; -namespace API.DTOs.CollectionTags; +namespace Kavita.Models.DTOs.CollectionTags; public sealed record UpdateSeriesForTagDto { diff --git a/API/DTOs/ColorScape.cs b/Kavita.Models/DTOs/ColorScape.cs similarity index 86% rename from API/DTOs/ColorScape.cs rename to Kavita.Models/DTOs/ColorScape.cs index 5351f2351..fbe2f247d 100644 --- a/API/DTOs/ColorScape.cs +++ b/Kavita.Models/DTOs/ColorScape.cs @@ -1,4 +1,4 @@ -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable /// diff --git a/API/DTOs/CopySettingsFromLibraryDto.cs b/Kavita.Models/DTOs/CopySettingsFromLibraryDto.cs similarity index 91% rename from API/DTOs/CopySettingsFromLibraryDto.cs rename to Kavita.Models/DTOs/CopySettingsFromLibraryDto.cs index 5ca5ead51..7c2df97f0 100644 --- a/API/DTOs/CopySettingsFromLibraryDto.cs +++ b/Kavita.Models/DTOs/CopySettingsFromLibraryDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record CopySettingsFromLibraryDto { diff --git a/API/DTOs/CoverDb/CoverDbAuthor.cs b/Kavita.Models/DTOs/CoverDb/CoverDbAuthor.cs similarity index 93% rename from API/DTOs/CoverDb/CoverDbAuthor.cs rename to Kavita.Models/DTOs/CoverDb/CoverDbAuthor.cs index ca924801f..316ea6479 100644 --- a/API/DTOs/CoverDb/CoverDbAuthor.cs +++ b/Kavita.Models/DTOs/CoverDb/CoverDbAuthor.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using YamlDotNet.Serialization; -namespace API.DTOs.CoverDb; +namespace Kavita.Models.DTOs.CoverDb; public sealed record CoverDbAuthor { diff --git a/API/DTOs/CoverDb/CoverDbPeople.cs b/Kavita.Models/DTOs/CoverDb/CoverDbPeople.cs similarity index 87% rename from API/DTOs/CoverDb/CoverDbPeople.cs rename to Kavita.Models/DTOs/CoverDb/CoverDbPeople.cs index 2e825eac7..0d591bcf0 100644 --- a/API/DTOs/CoverDb/CoverDbPeople.cs +++ b/Kavita.Models/DTOs/CoverDb/CoverDbPeople.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using YamlDotNet.Serialization; -namespace API.DTOs.CoverDb; +namespace Kavita.Models.DTOs.CoverDb; public sealed record CoverDbPeople { diff --git a/API/DTOs/CoverDb/CoverDbPersonIds.cs b/Kavita.Models/DTOs/CoverDb/CoverDbPersonIds.cs similarity index 95% rename from API/DTOs/CoverDb/CoverDbPersonIds.cs rename to Kavita.Models/DTOs/CoverDb/CoverDbPersonIds.cs index 5816bb479..11d43b19a 100644 --- a/API/DTOs/CoverDb/CoverDbPersonIds.cs +++ b/Kavita.Models/DTOs/CoverDb/CoverDbPersonIds.cs @@ -1,6 +1,6 @@ using YamlDotNet.Serialization; -namespace API.DTOs.CoverDb; +namespace Kavita.Models.DTOs.CoverDb; #nullable enable public sealed record CoverDbPersonIds diff --git a/API/DTOs/Dashboard/DashboardStreamDto.cs b/Kavita.Models/DTOs/Dashboard/DashboardStreamDto.cs similarity index 90% rename from API/DTOs/Dashboard/DashboardStreamDto.cs rename to Kavita.Models/DTOs/Dashboard/DashboardStreamDto.cs index 7ebbe6fb0..6234efb1e 100644 --- a/API/DTOs/Dashboard/DashboardStreamDto.cs +++ b/Kavita.Models/DTOs/Dashboard/DashboardStreamDto.cs @@ -1,6 +1,7 @@ -using API.Entities.Enums; + +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Dashboard; +namespace Kavita.Models.DTOs.Dashboard; public sealed record DashboardStreamDto { diff --git a/API/DTOs/Dashboard/GroupedSeriesDto.cs b/Kavita.Models/DTOs/Dashboard/GroupedSeriesDto.cs similarity index 93% rename from API/DTOs/Dashboard/GroupedSeriesDto.cs rename to Kavita.Models/DTOs/Dashboard/GroupedSeriesDto.cs index 940e42c40..d10778091 100644 --- a/API/DTOs/Dashboard/GroupedSeriesDto.cs +++ b/Kavita.Models/DTOs/Dashboard/GroupedSeriesDto.cs @@ -1,7 +1,7 @@ using System; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Dashboard; +namespace Kavita.Models.DTOs.Dashboard; /// /// This is a representation of a Series with some amount of underlying files within it. This is used for Recently Updated Series section /// diff --git a/API/DTOs/Dashboard/SmartFilterDto.cs b/Kavita.Models/DTOs/Dashboard/SmartFilterDto.cs similarity index 79% rename from API/DTOs/Dashboard/SmartFilterDto.cs rename to Kavita.Models/DTOs/Dashboard/SmartFilterDto.cs index c1bc4d7e1..ee734861a 100644 --- a/API/DTOs/Dashboard/SmartFilterDto.cs +++ b/Kavita.Models/DTOs/Dashboard/SmartFilterDto.cs @@ -1,6 +1,6 @@ -using API.DTOs.Filtering.v2; +using Kavita.Models.DTOs.Filtering.v2; -namespace API.DTOs.Dashboard; +namespace Kavita.Models.DTOs.Dashboard; public sealed record SmartFilterDto { diff --git a/API/DTOs/Dashboard/UpdateDashboardStreamPositionDto.cs b/Kavita.Models/DTOs/Dashboard/UpdateDashboardStreamPositionDto.cs similarity index 84% rename from API/DTOs/Dashboard/UpdateDashboardStreamPositionDto.cs rename to Kavita.Models/DTOs/Dashboard/UpdateDashboardStreamPositionDto.cs index 476a0732e..e2c9d4e95 100644 --- a/API/DTOs/Dashboard/UpdateDashboardStreamPositionDto.cs +++ b/Kavita.Models/DTOs/Dashboard/UpdateDashboardStreamPositionDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Dashboard; +namespace Kavita.Models.DTOs.Dashboard; public sealed record UpdateDashboardStreamPositionDto { diff --git a/API/DTOs/Dashboard/UpdateStreamPositionDto.cs b/Kavita.Models/DTOs/Dashboard/UpdateStreamPositionDto.cs similarity index 89% rename from API/DTOs/Dashboard/UpdateStreamPositionDto.cs rename to Kavita.Models/DTOs/Dashboard/UpdateStreamPositionDto.cs index 33b939a39..69085c92e 100644 --- a/API/DTOs/Dashboard/UpdateStreamPositionDto.cs +++ b/Kavita.Models/DTOs/Dashboard/UpdateStreamPositionDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Dashboard; +namespace Kavita.Models.DTOs.Dashboard; public sealed record UpdateStreamPositionDto { diff --git a/API/DTOs/DeleteChaptersDto.cs b/Kavita.Models/DTOs/DeleteChaptersDto.cs similarity index 82% rename from API/DTOs/DeleteChaptersDto.cs rename to Kavita.Models/DTOs/DeleteChaptersDto.cs index 9fad2f1fb..f2742d7fa 100644 --- a/API/DTOs/DeleteChaptersDto.cs +++ b/Kavita.Models/DTOs/DeleteChaptersDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record DeleteChaptersDto { diff --git a/API/DTOs/DeleteSeriesDto.cs b/Kavita.Models/DTOs/DeleteSeriesDto.cs similarity index 82% rename from API/DTOs/DeleteSeriesDto.cs rename to Kavita.Models/DTOs/DeleteSeriesDto.cs index ec9ba0c68..80f56ca37 100644 --- a/API/DTOs/DeleteSeriesDto.cs +++ b/Kavita.Models/DTOs/DeleteSeriesDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record DeleteSeriesDto { diff --git a/API/DTOs/Device/ClientDevice/UpdateClientDeviceNameDto.cs b/Kavita.Models/DTOs/Device/ClientDevice/UpdateClientDeviceNameDto.cs similarity index 70% rename from API/DTOs/Device/ClientDevice/UpdateClientDeviceNameDto.cs rename to Kavita.Models/DTOs/Device/ClientDevice/UpdateClientDeviceNameDto.cs index 7d4180ad5..167348d1f 100644 --- a/API/DTOs/Device/ClientDevice/UpdateClientDeviceNameDto.cs +++ b/Kavita.Models/DTOs/Device/ClientDevice/UpdateClientDeviceNameDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Device.ClientDevice; +namespace Kavita.Models.DTOs.Device.ClientDevice; public sealed record UpdateClientDeviceNameDto { diff --git a/API/DTOs/Device/EmailDevice/CreateEmailDeviceDto.cs b/Kavita.Models/DTOs/Device/EmailDevice/CreateEmailDeviceDto.cs similarity index 81% rename from API/DTOs/Device/EmailDevice/CreateEmailDeviceDto.cs rename to Kavita.Models/DTOs/Device/EmailDevice/CreateEmailDeviceDto.cs index 7b084a2f1..e63af801b 100644 --- a/API/DTOs/Device/EmailDevice/CreateEmailDeviceDto.cs +++ b/Kavita.Models/DTOs/Device/EmailDevice/CreateEmailDeviceDto.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations; -using API.Entities.Enums.Device; +using Kavita.Models.Entities.Enums.Device; -namespace API.DTOs.Device.EmailDevice; +namespace Kavita.Models.DTOs.Device.EmailDevice; public sealed record CreateEmailDeviceDto { diff --git a/API/DTOs/Device/EmailDevice/DeviceDto.cs b/Kavita.Models/DTOs/Device/EmailDevice/DeviceDto.cs similarity index 89% rename from API/DTOs/Device/EmailDevice/DeviceDto.cs rename to Kavita.Models/DTOs/Device/EmailDevice/DeviceDto.cs index a6b8ba3b4..3cdc1d6a4 100644 --- a/API/DTOs/Device/EmailDevice/DeviceDto.cs +++ b/Kavita.Models/DTOs/Device/EmailDevice/DeviceDto.cs @@ -1,6 +1,8 @@ -using API.Entities.Enums.Device; + -namespace API.DTOs.Device.EmailDevice; +using Kavita.Models.Entities.Enums.Device; + +namespace Kavita.Models.DTOs.Device.EmailDevice; /// /// A Device is an entity that can receive data from Kavita (kindle) diff --git a/API/DTOs/Device/EmailDevice/SendSeriesToEmailDeviceDto.cs b/Kavita.Models/DTOs/Device/EmailDevice/SendSeriesToEmailDeviceDto.cs similarity index 71% rename from API/DTOs/Device/EmailDevice/SendSeriesToEmailDeviceDto.cs rename to Kavita.Models/DTOs/Device/EmailDevice/SendSeriesToEmailDeviceDto.cs index a97864c66..0346a2da1 100644 --- a/API/DTOs/Device/EmailDevice/SendSeriesToEmailDeviceDto.cs +++ b/Kavita.Models/DTOs/Device/EmailDevice/SendSeriesToEmailDeviceDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Device.EmailDevice; +namespace Kavita.Models.DTOs.Device.EmailDevice; public sealed record SendSeriesToEmailDeviceDto { diff --git a/API/DTOs/Device/EmailDevice/SendToEmailDeviceDto.cs b/Kavita.Models/DTOs/Device/EmailDevice/SendToEmailDeviceDto.cs similarity index 79% rename from API/DTOs/Device/EmailDevice/SendToEmailDeviceDto.cs rename to Kavita.Models/DTOs/Device/EmailDevice/SendToEmailDeviceDto.cs index 313ac080a..3c8c0e896 100644 --- a/API/DTOs/Device/EmailDevice/SendToEmailDeviceDto.cs +++ b/Kavita.Models/DTOs/Device/EmailDevice/SendToEmailDeviceDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Device.EmailDevice; +namespace Kavita.Models.DTOs.Device.EmailDevice; public sealed record SendToEmailDeviceDto { diff --git a/API/DTOs/Device/EmailDevice/UpdateDeviceDto.cs b/Kavita.Models/DTOs/Device/EmailDevice/UpdateDeviceDto.cs similarity index 83% rename from API/DTOs/Device/EmailDevice/UpdateDeviceDto.cs rename to Kavita.Models/DTOs/Device/EmailDevice/UpdateDeviceDto.cs index 0346bd207..32e150036 100644 --- a/API/DTOs/Device/EmailDevice/UpdateDeviceDto.cs +++ b/Kavita.Models/DTOs/Device/EmailDevice/UpdateDeviceDto.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations; -using API.Entities.Enums.Device; +using Kavita.Models.Entities.Enums.Device; -namespace API.DTOs.Device.EmailDevice; +namespace Kavita.Models.DTOs.Device.EmailDevice; public sealed record UpdateEmailDeviceDto { diff --git a/API/DTOs/Downloads/DownloadBookmarkDto.cs b/Kavita.Models/DTOs/Downloads/DownloadBookmarkDto.cs similarity index 74% rename from API/DTOs/Downloads/DownloadBookmarkDto.cs rename to Kavita.Models/DTOs/Downloads/DownloadBookmarkDto.cs index 00f763dac..8c162dfb0 100644 --- a/API/DTOs/Downloads/DownloadBookmarkDto.cs +++ b/Kavita.Models/DTOs/Downloads/DownloadBookmarkDto.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using API.DTOs.Reader; +using Kavita.Models.DTOs.Reader; -namespace API.DTOs.Downloads; +namespace Kavita.Models.DTOs.Downloads; public sealed record DownloadBookmarkDto { diff --git a/API/DTOs/Email/ConfirmationEmailDto.cs b/Kavita.Models/DTOs/Email/ConfirmationEmailDto.cs similarity index 90% rename from API/DTOs/Email/ConfirmationEmailDto.cs rename to Kavita.Models/DTOs/Email/ConfirmationEmailDto.cs index 197395794..bbf3d821a 100644 --- a/API/DTOs/Email/ConfirmationEmailDto.cs +++ b/Kavita.Models/DTOs/Email/ConfirmationEmailDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Email; +namespace Kavita.Models.DTOs.Email; public sealed record ConfirmationEmailDto { diff --git a/API/DTOs/Email/EmailHistoryDto.cs b/Kavita.Models/DTOs/Email/EmailHistoryDto.cs similarity index 90% rename from API/DTOs/Email/EmailHistoryDto.cs rename to Kavita.Models/DTOs/Email/EmailHistoryDto.cs index c2968d091..16ff4ec2f 100644 --- a/API/DTOs/Email/EmailHistoryDto.cs +++ b/Kavita.Models/DTOs/Email/EmailHistoryDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.Email; +namespace Kavita.Models.DTOs.Email; public sealed record EmailHistoryDto { diff --git a/API/DTOs/Email/EmailMigrationDto.cs b/Kavita.Models/DTOs/Email/EmailMigrationDto.cs similarity index 90% rename from API/DTOs/Email/EmailMigrationDto.cs rename to Kavita.Models/DTOs/Email/EmailMigrationDto.cs index 5354afdaa..524b92a73 100644 --- a/API/DTOs/Email/EmailMigrationDto.cs +++ b/Kavita.Models/DTOs/Email/EmailMigrationDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Email; +namespace Kavita.Models.DTOs.Email; public sealed record EmailMigrationDto { diff --git a/API/DTOs/Email/EmailTestResultDto.cs b/Kavita.Models/DTOs/Email/EmailTestResultDto.cs similarity index 89% rename from API/DTOs/Email/EmailTestResultDto.cs rename to Kavita.Models/DTOs/Email/EmailTestResultDto.cs index 9be868eab..28dea1f30 100644 --- a/API/DTOs/Email/EmailTestResultDto.cs +++ b/Kavita.Models/DTOs/Email/EmailTestResultDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Email; +namespace Kavita.Models.DTOs.Email; /// /// Represents if Test Email Service URL was successful or not and if any error occured diff --git a/API/DTOs/Email/PasswordResetEmailDto.cs b/Kavita.Models/DTOs/Email/PasswordResetEmailDto.cs similarity index 88% rename from API/DTOs/Email/PasswordResetEmailDto.cs rename to Kavita.Models/DTOs/Email/PasswordResetEmailDto.cs index 9fda066a9..c98af7143 100644 --- a/API/DTOs/Email/PasswordResetEmailDto.cs +++ b/Kavita.Models/DTOs/Email/PasswordResetEmailDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Email; +namespace Kavita.Models.DTOs.Email; public sealed record PasswordResetEmailDto { diff --git a/API/DTOs/Email/SendToDto.cs b/Kavita.Models/DTOs/Email/SendToDto.cs similarity index 84% rename from API/DTOs/Email/SendToDto.cs rename to Kavita.Models/DTOs/Email/SendToDto.cs index eacd29449..0cb0d4ad7 100644 --- a/API/DTOs/Email/SendToDto.cs +++ b/Kavita.Models/DTOs/Email/SendToDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Email; +namespace Kavita.Models.DTOs.Email; public sealed record SendToDto { diff --git a/API/DTOs/Email/TestEmailDto.cs b/Kavita.Models/DTOs/Email/TestEmailDto.cs similarity index 69% rename from API/DTOs/Email/TestEmailDto.cs rename to Kavita.Models/DTOs/Email/TestEmailDto.cs index 44c11bd6c..93843ff6f 100644 --- a/API/DTOs/Email/TestEmailDto.cs +++ b/Kavita.Models/DTOs/Email/TestEmailDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Email; +namespace Kavita.Models.DTOs.Email; public sealed record TestEmailDto { diff --git a/API/DTOs/Filtering/FilterDto.cs b/Kavita.Models/DTOs/Filtering/FilterDto.cs similarity index 97% rename from API/DTOs/Filtering/FilterDto.cs rename to Kavita.Models/DTOs/Filtering/FilterDto.cs index cb3374838..e4d40b752 100644 --- a/API/DTOs/Filtering/FilterDto.cs +++ b/Kavita.Models/DTOs/Filtering/FilterDto.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; -using API.Entities; -using API.Entities.Enums; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; -namespace API.DTOs.Filtering; -#nullable enable +namespace Kavita.Models.DTOs.Filtering; public sealed record FilterDto { diff --git a/API/DTOs/Filtering/LanguageDto.cs b/Kavita.Models/DTOs/Filtering/LanguageDto.cs similarity index 75% rename from API/DTOs/Filtering/LanguageDto.cs rename to Kavita.Models/DTOs/Filtering/LanguageDto.cs index dde85f07e..1e116e957 100644 --- a/API/DTOs/Filtering/LanguageDto.cs +++ b/Kavita.Models/DTOs/Filtering/LanguageDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Filtering; +namespace Kavita.Models.DTOs.Filtering; public sealed record LanguageDto { diff --git a/API/DTOs/Filtering/PersonSortField.cs b/Kavita.Models/DTOs/Filtering/PersonSortField.cs similarity index 67% rename from API/DTOs/Filtering/PersonSortField.cs rename to Kavita.Models/DTOs/Filtering/PersonSortField.cs index 5268a1bf9..079fefb1a 100644 --- a/API/DTOs/Filtering/PersonSortField.cs +++ b/Kavita.Models/DTOs/Filtering/PersonSortField.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Filtering; +namespace Kavita.Models.DTOs.Filtering; public enum PersonSortField { diff --git a/API/DTOs/Filtering/Range.cs b/Kavita.Models/DTOs/Filtering/Range.cs similarity index 86% rename from API/DTOs/Filtering/Range.cs rename to Kavita.Models/DTOs/Filtering/Range.cs index e697f26e1..e80f49fa4 100644 --- a/API/DTOs/Filtering/Range.cs +++ b/Kavita.Models/DTOs/Filtering/Range.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Filtering; +namespace Kavita.Models.DTOs.Filtering; #nullable enable /// diff --git a/API/DTOs/Filtering/ReadStatus.cs b/Kavita.Models/DTOs/Filtering/ReadStatus.cs similarity index 86% rename from API/DTOs/Filtering/ReadStatus.cs rename to Kavita.Models/DTOs/Filtering/ReadStatus.cs index 81498ecb5..9b1dbb4a5 100644 --- a/API/DTOs/Filtering/ReadStatus.cs +++ b/Kavita.Models/DTOs/Filtering/ReadStatus.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Filtering; +namespace Kavita.Models.DTOs.Filtering; /// /// Represents the Reading Status. This is a flag and allows multiple statues diff --git a/API/DTOs/Filtering/SortField.cs b/Kavita.Models/DTOs/Filtering/SortField.cs similarity index 96% rename from API/DTOs/Filtering/SortField.cs rename to Kavita.Models/DTOs/Filtering/SortField.cs index eaecea0c9..c8346bd18 100644 --- a/API/DTOs/Filtering/SortField.cs +++ b/Kavita.Models/DTOs/Filtering/SortField.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Filtering; +namespace Kavita.Models.DTOs.Filtering; public enum SortField { diff --git a/API/DTOs/Filtering/SortOptions.cs b/Kavita.Models/DTOs/Filtering/SortOptions.cs similarity index 94% rename from API/DTOs/Filtering/SortOptions.cs rename to Kavita.Models/DTOs/Filtering/SortOptions.cs index 864801e6b..96d91f147 100644 --- a/API/DTOs/Filtering/SortOptions.cs +++ b/Kavita.Models/DTOs/Filtering/SortOptions.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Filtering; +namespace Kavita.Models.DTOs.Filtering; /// /// Sorting Options for a query diff --git a/API/DTOs/Filtering/v2/DecodeFilterDto.cs b/Kavita.Models/DTOs/Filtering/v2/DecodeFilterDto.cs similarity index 78% rename from API/DTOs/Filtering/v2/DecodeFilterDto.cs rename to Kavita.Models/DTOs/Filtering/v2/DecodeFilterDto.cs index db4c7ecce..821c99f92 100644 --- a/API/DTOs/Filtering/v2/DecodeFilterDto.cs +++ b/Kavita.Models/DTOs/Filtering/v2/DecodeFilterDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Filtering.v2; +namespace Kavita.Models.DTOs.Filtering.v2; /// /// For requesting an encoded filter to be decoded diff --git a/API/DTOs/Filtering/v2/FilterCombination.cs b/Kavita.Models/DTOs/Filtering/v2/FilterCombination.cs similarity index 56% rename from API/DTOs/Filtering/v2/FilterCombination.cs rename to Kavita.Models/DTOs/Filtering/v2/FilterCombination.cs index d011cb000..a5e4da194 100644 --- a/API/DTOs/Filtering/v2/FilterCombination.cs +++ b/Kavita.Models/DTOs/Filtering/v2/FilterCombination.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Filtering.v2; +namespace Kavita.Models.DTOs.Filtering.v2; public enum FilterCombination { diff --git a/API/DTOs/Filtering/v2/FilterComparision.cs b/Kavita.Models/DTOs/Filtering/v2/FilterComparision.cs similarity index 97% rename from API/DTOs/Filtering/v2/FilterComparision.cs rename to Kavita.Models/DTOs/Filtering/v2/FilterComparision.cs index 59bb86a8a..400ed32dd 100644 --- a/API/DTOs/Filtering/v2/FilterComparision.cs +++ b/Kavita.Models/DTOs/Filtering/v2/FilterComparision.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.DTOs.Filtering.v2; +namespace Kavita.Models.DTOs.Filtering.v2; public enum FilterComparison { diff --git a/API/DTOs/Filtering/v2/FilterField.cs b/Kavita.Models/DTOs/Filtering/v2/FilterField.cs similarity index 97% rename from API/DTOs/Filtering/v2/FilterField.cs rename to Kavita.Models/DTOs/Filtering/v2/FilterField.cs index 654fc64cf..a345873da 100644 --- a/API/DTOs/Filtering/v2/FilterField.cs +++ b/Kavita.Models/DTOs/Filtering/v2/FilterField.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Filtering.v2; +namespace Kavita.Models.DTOs.Filtering.v2; /// /// Represents the field which will dictate the value type and the Extension used for filtering diff --git a/API/DTOs/Filtering/v2/FilterStatementDto.cs b/Kavita.Models/DTOs/Filtering/v2/FilterStatementDto.cs similarity index 93% rename from API/DTOs/Filtering/v2/FilterStatementDto.cs rename to Kavita.Models/DTOs/Filtering/v2/FilterStatementDto.cs index 47d87e94c..2e583a220 100644 --- a/API/DTOs/Filtering/v2/FilterStatementDto.cs +++ b/Kavita.Models/DTOs/Filtering/v2/FilterStatementDto.cs @@ -1,5 +1,5 @@  -namespace API.DTOs.Filtering.v2; +namespace Kavita.Models.DTOs.Filtering.v2; public sealed record FilterStatementDto { diff --git a/API/DTOs/Filtering/v2/FilterV2Dto.cs b/Kavita.Models/DTOs/Filtering/v2/FilterV2Dto.cs similarity index 94% rename from API/DTOs/Filtering/v2/FilterV2Dto.cs rename to Kavita.Models/DTOs/Filtering/v2/FilterV2Dto.cs index dc30b26e2..94d16f4f3 100644 --- a/API/DTOs/Filtering/v2/FilterV2Dto.cs +++ b/Kavita.Models/DTOs/Filtering/v2/FilterV2Dto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Filtering.v2; +namespace Kavita.Models.DTOs.Filtering.v2; #nullable enable /// diff --git a/API/DTOs/Font/EpubFontDto.cs b/Kavita.Models/DTOs/Font/EpubFontDto.cs similarity index 72% rename from API/DTOs/Font/EpubFontDto.cs rename to Kavita.Models/DTOs/Font/EpubFontDto.cs index 4a85916dc..b32e61640 100644 --- a/API/DTOs/Font/EpubFontDto.cs +++ b/Kavita.Models/DTOs/Font/EpubFontDto.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums.Font; +using Kavita.Models.Entities.Enums.Font; -namespace API.DTOs.Font; +namespace Kavita.Models.DTOs.Font; public sealed record EpubFontDto { diff --git a/API/DTOs/ImportFieldMappings.cs b/Kavita.Models/DTOs/ImportFieldMappings.cs similarity index 96% rename from API/DTOs/ImportFieldMappings.cs rename to Kavita.Models/DTOs/ImportFieldMappings.cs index 41d7ab748..d961588d7 100644 --- a/API/DTOs/ImportFieldMappings.cs +++ b/Kavita.Models/DTOs/ImportFieldMappings.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.ComponentModel; -using API.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.KavitaPlus.Metadata; -namespace API.DTOs; +namespace Kavita.Models.DTOs; /// /// How Kavita should import the new settings diff --git a/API/DTOs/Internal/AppSettingsDto.cs b/Kavita.Models/DTOs/Internal/AppSettingsDto.cs similarity index 86% rename from API/DTOs/Internal/AppSettingsDto.cs rename to Kavita.Models/DTOs/Internal/AppSettingsDto.cs index 0aa36c7d7..2add8e7cb 100644 --- a/API/DTOs/Internal/AppSettingsDto.cs +++ b/Kavita.Models/DTOs/Internal/AppSettingsDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Internal; +namespace Kavita.Models.DTOs.Internal; #nullable enable public sealed record AppSettingsDto diff --git a/API/DTOs/Jobs/JobDto.cs b/Kavita.Models/DTOs/Jobs/JobDto.cs similarity index 94% rename from API/DTOs/Jobs/JobDto.cs rename to Kavita.Models/DTOs/Jobs/JobDto.cs index 55419811f..a962a445e 100644 --- a/API/DTOs/Jobs/JobDto.cs +++ b/Kavita.Models/DTOs/Jobs/JobDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.Jobs; +namespace Kavita.Models.DTOs.Jobs; public sealed record JobDto { diff --git a/API/DTOs/JumpBar/JumpKeyDto.cs b/Kavita.Models/DTOs/JumpBar/JumpKeyDto.cs similarity index 91% rename from API/DTOs/JumpBar/JumpKeyDto.cs rename to Kavita.Models/DTOs/JumpBar/JumpKeyDto.cs index 8dc5b4a8e..9165905be 100644 --- a/API/DTOs/JumpBar/JumpKeyDto.cs +++ b/Kavita.Models/DTOs/JumpBar/JumpKeyDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.JumpBar; +namespace Kavita.Models.DTOs.JumpBar; /// /// Represents an individual button in a Jump Bar diff --git a/API/DTOs/KavitaLocale.cs b/Kavita.Models/DTOs/KavitaLocale.cs similarity index 90% rename from API/DTOs/KavitaLocale.cs rename to Kavita.Models/DTOs/KavitaLocale.cs index 51868605f..a71f665b6 100644 --- a/API/DTOs/KavitaLocale.cs +++ b/Kavita.Models/DTOs/KavitaLocale.cs @@ -1,4 +1,4 @@ -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record KavitaLocale { diff --git a/API/DTOs/KavitaPlus/Account/AniListUpdateDto.cs b/Kavita.Models/DTOs/KavitaPlus/Account/AniListUpdateDto.cs similarity index 60% rename from API/DTOs/KavitaPlus/Account/AniListUpdateDto.cs rename to Kavita.Models/DTOs/KavitaPlus/Account/AniListUpdateDto.cs index c053bd34e..1c76c6cc9 100644 --- a/API/DTOs/KavitaPlus/Account/AniListUpdateDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/Account/AniListUpdateDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.KavitaPlus.Account; +namespace Kavita.Models.DTOs.KavitaPlus.Account; public sealed record AniListUpdateDto { diff --git a/API/DTOs/KavitaPlus/Account/UserTokenInfo.cs b/Kavita.Models/DTOs/KavitaPlus/Account/UserTokenInfo.cs similarity index 89% rename from API/DTOs/KavitaPlus/Account/UserTokenInfo.cs rename to Kavita.Models/DTOs/KavitaPlus/Account/UserTokenInfo.cs index 340ad0f4c..baa35203e 100644 --- a/API/DTOs/KavitaPlus/Account/UserTokenInfo.cs +++ b/Kavita.Models/DTOs/KavitaPlus/Account/UserTokenInfo.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.KavitaPlus.Account; +namespace Kavita.Models.DTOs.KavitaPlus.Account; /// /// Represents information around a user's tokens and their status diff --git a/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs b/Kavita.Models/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs similarity index 81% rename from API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs rename to Kavita.Models/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs index c05ff0567..410a37228 100644 --- a/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs @@ -1,6 +1,6 @@ -using API.DTOs.Scrobbling; +using Kavita.Models.DTOs.Scrobbling; -namespace API.DTOs.KavitaPlus.ExternalMetadata; +namespace Kavita.Models.DTOs.KavitaPlus.ExternalMetadata; #nullable enable /// diff --git a/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs b/Kavita.Models/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs similarity index 86% rename from API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs rename to Kavita.Models/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs index a7359d69b..5178159aa 100644 --- a/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using API.DTOs.Scrobbling; +using Kavita.Models.DTOs.Scrobbling; -namespace API.DTOs.KavitaPlus.ExternalMetadata; +namespace Kavita.Models.DTOs.KavitaPlus.ExternalMetadata; #nullable enable /// diff --git a/API/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs b/Kavita.Models/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs similarity index 71% rename from API/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs rename to Kavita.Models/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs index 48d2a2095..bc59e7fed 100644 --- a/API/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Scrobbling; -using API.DTOs.SeriesDetail; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.DTOs.SeriesDetail; -namespace API.DTOs.KavitaPlus.ExternalMetadata; +namespace Kavita.Models.DTOs.KavitaPlus.ExternalMetadata; public sealed record SeriesDetailPlusApiDto { diff --git a/API/DTOs/KavitaPlus/License/EncryptLicenseDto.cs b/Kavita.Models/DTOs/KavitaPlus/License/EncryptLicenseDto.cs similarity index 82% rename from API/DTOs/KavitaPlus/License/EncryptLicenseDto.cs rename to Kavita.Models/DTOs/KavitaPlus/License/EncryptLicenseDto.cs index dd85dd063..bd0f3e82a 100644 --- a/API/DTOs/KavitaPlus/License/EncryptLicenseDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/License/EncryptLicenseDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.KavitaPlus.License; +namespace Kavita.Models.DTOs.KavitaPlus.License; #nullable enable public sealed record EncryptLicenseDto diff --git a/API/DTOs/KavitaPlus/License/LicenseInfoDto.cs b/Kavita.Models/DTOs/KavitaPlus/License/LicenseInfoDto.cs similarity index 95% rename from API/DTOs/KavitaPlus/License/LicenseInfoDto.cs rename to Kavita.Models/DTOs/KavitaPlus/License/LicenseInfoDto.cs index aaf4eded8..f4015e773 100644 --- a/API/DTOs/KavitaPlus/License/LicenseInfoDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/License/LicenseInfoDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.KavitaPlus.License; +namespace Kavita.Models.DTOs.KavitaPlus.License; public sealed record LicenseInfoDto { diff --git a/API/DTOs/KavitaPlus/License/LicenseValidDto.cs b/Kavita.Models/DTOs/KavitaPlus/License/LicenseValidDto.cs similarity index 73% rename from API/DTOs/KavitaPlus/License/LicenseValidDto.cs rename to Kavita.Models/DTOs/KavitaPlus/License/LicenseValidDto.cs index a7bd476ce..25f112aeb 100644 --- a/API/DTOs/KavitaPlus/License/LicenseValidDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/License/LicenseValidDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.KavitaPlus.License; +namespace Kavita.Models.DTOs.KavitaPlus.License; public sealed record LicenseValidDto { diff --git a/API/DTOs/KavitaPlus/License/ResetLicenseDto.cs b/Kavita.Models/DTOs/KavitaPlus/License/ResetLicenseDto.cs similarity index 78% rename from API/DTOs/KavitaPlus/License/ResetLicenseDto.cs rename to Kavita.Models/DTOs/KavitaPlus/License/ResetLicenseDto.cs index d0fd9b666..5933b4f44 100644 --- a/API/DTOs/KavitaPlus/License/ResetLicenseDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/License/ResetLicenseDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.KavitaPlus.License; +namespace Kavita.Models.DTOs.KavitaPlus.License; public sealed record ResetLicenseDto { diff --git a/API/DTOs/KavitaPlus/License/UpdateLicenseDto.cs b/Kavita.Models/DTOs/KavitaPlus/License/UpdateLicenseDto.cs similarity index 88% rename from API/DTOs/KavitaPlus/License/UpdateLicenseDto.cs rename to Kavita.Models/DTOs/KavitaPlus/License/UpdateLicenseDto.cs index 28b47efbe..cf06fae88 100644 --- a/API/DTOs/KavitaPlus/License/UpdateLicenseDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/License/UpdateLicenseDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.KavitaPlus.License; +namespace Kavita.Models.DTOs.KavitaPlus.License; #nullable enable public sealed record UpdateLicenseDto diff --git a/API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs b/Kavita.Models/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs similarity index 91% rename from API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs rename to Kavita.Models/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs index c394cf8d4..6a7e4e256 100644 --- a/API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.KavitaPlus.Manage; +namespace Kavita.Models.DTOs.KavitaPlus.Manage; /// /// Represents an option in the UI layer for Filtering diff --git a/API/DTOs/KavitaPlus/Manage/ManageMatchSeriesDto.cs b/Kavita.Models/DTOs/KavitaPlus/Manage/ManageMatchSeriesDto.cs similarity index 80% rename from API/DTOs/KavitaPlus/Manage/ManageMatchSeriesDto.cs rename to Kavita.Models/DTOs/KavitaPlus/Manage/ManageMatchSeriesDto.cs index a51e63ee9..bc18d74bb 100644 --- a/API/DTOs/KavitaPlus/Manage/ManageMatchSeriesDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/Manage/ManageMatchSeriesDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.KavitaPlus.Manage; +namespace Kavita.Models.DTOs.KavitaPlus.Manage; public sealed record ManageMatchSeriesDto { diff --git a/API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs b/Kavita.Models/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs similarity index 90% rename from API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs rename to Kavita.Models/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs index add9ca723..293585d17 100644 --- a/API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using API.DTOs.SeriesDetail; +using Kavita.Models.DTOs.SeriesDetail; -namespace API.DTOs.KavitaPlus.Metadata; +namespace Kavita.Models.DTOs.KavitaPlus.Metadata; #nullable enable /// diff --git a/API/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs b/Kavita.Models/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs similarity index 88% rename from API/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs rename to Kavita.Models/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs index 6704bf697..5c43c84d0 100644 --- a/API/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs @@ -1,11 +1,11 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; -using API.DTOs.Recommendation; -using API.DTOs.Scrobbling; -using API.Services.Plus; +using Kavita.Models.DTOs.Recommendation; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.KavitaPlus.Metadata; -#nullable enable +namespace Kavita.Models.DTOs.KavitaPlus.Metadata; /// /// This is AniListSeries diff --git a/API/DTOs/KavitaPlus/Metadata/MetadataFieldMappingDto.cs b/Kavita.Models/DTOs/KavitaPlus/Metadata/MetadataFieldMappingDto.cs similarity index 87% rename from API/DTOs/KavitaPlus/Metadata/MetadataFieldMappingDto.cs rename to Kavita.Models/DTOs/KavitaPlus/Metadata/MetadataFieldMappingDto.cs index a9debabd1..e9b7ff9a2 100644 --- a/API/DTOs/KavitaPlus/Metadata/MetadataFieldMappingDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/Metadata/MetadataFieldMappingDto.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.KavitaPlus.Metadata; +namespace Kavita.Models.DTOs.KavitaPlus.Metadata; public sealed record MetadataFieldMappingDto { diff --git a/API/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs b/Kavita.Models/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs similarity index 96% rename from API/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs rename to Kavita.Models/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs index 97e743bce..481ac700f 100644 --- a/API/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs +++ b/Kavita.Models/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; -using API.DTOs.Settings; -using API.Entities.Enums; -using API.Entities.MetadataMatching; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.MetadataMatching; -namespace API.DTOs.KavitaPlus.Metadata; +namespace Kavita.Models.DTOs.KavitaPlus.Metadata; public sealed record MetadataSettingsDto: FieldMappingsDto diff --git a/API/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs b/Kavita.Models/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs similarity index 87% rename from API/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs rename to Kavita.Models/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs index 2b57548cd..fff85e9a6 100644 --- a/API/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs +++ b/Kavita.Models/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.KavitaPlus.Metadata; +namespace Kavita.Models.DTOs.KavitaPlus.Metadata; #nullable enable public enum CharacterRole diff --git a/API/DTOs/KavitaPlus/Metadata/SeriesRelationship.cs b/Kavita.Models/DTOs/KavitaPlus/Metadata/SeriesRelationship.cs similarity index 79% rename from API/DTOs/KavitaPlus/Metadata/SeriesRelationship.cs rename to Kavita.Models/DTOs/KavitaPlus/Metadata/SeriesRelationship.cs index 6178c1d23..1b7a5ef2b 100644 --- a/API/DTOs/KavitaPlus/Metadata/SeriesRelationship.cs +++ b/Kavita.Models/DTOs/KavitaPlus/Metadata/SeriesRelationship.cs @@ -1,8 +1,8 @@ -using API.DTOs.Scrobbling; -using API.Entities.Enums; -using API.Services.Plus; +#nullable enable +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.KavitaPlus.Metadata; +namespace Kavita.Models.DTOs.KavitaPlus.Metadata; public sealed record ALMediaTitle { diff --git a/API/DTOs/Koreader/KoreaderBookDto.cs b/Kavita.Models/DTOs/Koreader/KoreaderBookDto.cs similarity index 94% rename from API/DTOs/Koreader/KoreaderBookDto.cs rename to Kavita.Models/DTOs/Koreader/KoreaderBookDto.cs index 9bfc4adc3..7545ce057 100644 --- a/API/DTOs/Koreader/KoreaderBookDto.cs +++ b/Kavita.Models/DTOs/Koreader/KoreaderBookDto.cs @@ -1,6 +1,6 @@ -using API.DTOs.Progress; +using Kavita.Models.DTOs.Progress; -namespace API.DTOs.Koreader; +namespace Kavita.Models.DTOs.Koreader; /// /// This is the interface for receiving and sending updates to Koreader. The only fields diff --git a/API/DTOs/Koreader/KoreaderProgressUpdateDto.cs b/Kavita.Models/DTOs/Koreader/KoreaderProgressUpdateDto.cs similarity index 89% rename from API/DTOs/Koreader/KoreaderProgressUpdateDto.cs rename to Kavita.Models/DTOs/Koreader/KoreaderProgressUpdateDto.cs index 52a1d6cbd..9d401d656 100644 --- a/API/DTOs/Koreader/KoreaderProgressUpdateDto.cs +++ b/Kavita.Models/DTOs/Koreader/KoreaderProgressUpdateDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.Koreader; +namespace Kavita.Models.DTOs.Koreader; public class KoreaderProgressUpdateDto { diff --git a/API/DTOs/LibraryDto.cs b/Kavita.Models/DTOs/LibraryDto.cs similarity index 97% rename from API/DTOs/LibraryDto.cs rename to Kavita.Models/DTOs/LibraryDto.cs index a493b3626..9dc5e6e3e 100644 --- a/API/DTOs/LibraryDto.cs +++ b/Kavita.Models/DTOs/LibraryDto.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; -using API.Entities; -using API.Entities.Enums; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable /// diff --git a/API/DTOs/MangaFileDto.cs b/Kavita.Models/DTOs/MangaFileDto.cs similarity index 92% rename from API/DTOs/MangaFileDto.cs rename to Kavita.Models/DTOs/MangaFileDto.cs index 645c9ad32..afd0ee9db 100644 --- a/API/DTOs/MangaFileDto.cs +++ b/Kavita.Models/DTOs/MangaFileDto.cs @@ -1,7 +1,7 @@ using System; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable public sealed record MangaFileDto diff --git a/API/DTOs/MediaErrors/MediaErrorDto.cs b/Kavita.Models/DTOs/MediaErrors/MediaErrorDto.cs similarity index 93% rename from API/DTOs/MediaErrors/MediaErrorDto.cs rename to Kavita.Models/DTOs/MediaErrors/MediaErrorDto.cs index b77ee88be..4bac5aab2 100644 --- a/API/DTOs/MediaErrors/MediaErrorDto.cs +++ b/Kavita.Models/DTOs/MediaErrors/MediaErrorDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.MediaErrors; +namespace Kavita.Models.DTOs.MediaErrors; public sealed record MediaErrorDto { diff --git a/API/DTOs/Metadata/AgeRatingDto.cs b/Kavita.Models/DTOs/Metadata/AgeRatingDto.cs similarity index 62% rename from API/DTOs/Metadata/AgeRatingDto.cs rename to Kavita.Models/DTOs/Metadata/AgeRatingDto.cs index bfa835ef5..ad444c433 100644 --- a/API/DTOs/Metadata/AgeRatingDto.cs +++ b/Kavita.Models/DTOs/Metadata/AgeRatingDto.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Metadata; +namespace Kavita.Models.DTOs.Metadata; public sealed record AgeRatingDto { diff --git a/API/DTOs/Metadata/Browse/BrowseGenreDto.cs b/Kavita.Models/DTOs/Metadata/Browse/BrowseGenreDto.cs similarity index 85% rename from API/DTOs/Metadata/Browse/BrowseGenreDto.cs rename to Kavita.Models/DTOs/Metadata/Browse/BrowseGenreDto.cs index 8044c7914..76b846d50 100644 --- a/API/DTOs/Metadata/Browse/BrowseGenreDto.cs +++ b/Kavita.Models/DTOs/Metadata/Browse/BrowseGenreDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Metadata.Browse; +namespace Kavita.Models.DTOs.Metadata.Browse; public sealed record BrowseGenreDto : GenreTagDto { diff --git a/API/DTOs/Metadata/Browse/BrowsePersonDto.cs b/Kavita.Models/DTOs/Metadata/Browse/BrowsePersonDto.cs similarity index 83% rename from API/DTOs/Metadata/Browse/BrowsePersonDto.cs rename to Kavita.Models/DTOs/Metadata/Browse/BrowsePersonDto.cs index 20f84b783..c69650e9e 100644 --- a/API/DTOs/Metadata/Browse/BrowsePersonDto.cs +++ b/Kavita.Models/DTOs/Metadata/Browse/BrowsePersonDto.cs @@ -1,6 +1,6 @@ -using API.DTOs.Person; +using Kavita.Models.DTOs.Person; -namespace API.DTOs.Metadata.Browse; +namespace Kavita.Models.DTOs.Metadata.Browse; /// /// Used to browse writers and click in to see their series diff --git a/API/DTOs/Metadata/Browse/BrowseTagDto.cs b/Kavita.Models/DTOs/Metadata/Browse/BrowseTagDto.cs similarity index 85% rename from API/DTOs/Metadata/Browse/BrowseTagDto.cs rename to Kavita.Models/DTOs/Metadata/Browse/BrowseTagDto.cs index 9a71876e3..f755188ff 100644 --- a/API/DTOs/Metadata/Browse/BrowseTagDto.cs +++ b/Kavita.Models/DTOs/Metadata/Browse/BrowseTagDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Metadata.Browse; +namespace Kavita.Models.DTOs.Metadata.Browse; public sealed record BrowseTagDto : TagDto { diff --git a/API/DTOs/Metadata/Browse/Requests/BrowseAnnotationFilterDto.cs b/Kavita.Models/DTOs/Metadata/Browse/Requests/BrowseAnnotationFilterDto.cs similarity index 84% rename from API/DTOs/Metadata/Browse/Requests/BrowseAnnotationFilterDto.cs rename to Kavita.Models/DTOs/Metadata/Browse/Requests/BrowseAnnotationFilterDto.cs index 4a0f08cda..42204bc0a 100644 --- a/API/DTOs/Metadata/Browse/Requests/BrowseAnnotationFilterDto.cs +++ b/Kavita.Models/DTOs/Metadata/Browse/Requests/BrowseAnnotationFilterDto.cs @@ -1,9 +1,9 @@ #nullable enable using System.Collections.Generic; -using API.DTOs.Filtering; -using API.DTOs.Filtering.v2; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.Filtering.v2; -namespace API.DTOs.Metadata.Browse.Requests; +namespace Kavita.Models.DTOs.Metadata.Browse.Requests; public class BrowseAnnotationFilterDto { diff --git a/API/DTOs/Metadata/Browse/Requests/BrowsePersonFilterDto.cs b/Kavita.Models/DTOs/Metadata/Browse/Requests/BrowsePersonFilterDto.cs similarity index 84% rename from API/DTOs/Metadata/Browse/Requests/BrowsePersonFilterDto.cs rename to Kavita.Models/DTOs/Metadata/Browse/Requests/BrowsePersonFilterDto.cs index 26377591f..d96df333d 100644 --- a/API/DTOs/Metadata/Browse/Requests/BrowsePersonFilterDto.cs +++ b/Kavita.Models/DTOs/Metadata/Browse/Requests/BrowsePersonFilterDto.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; -using API.DTOs.Filtering; -using API.DTOs.Filtering.v2; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.Filtering.v2; -namespace API.DTOs.Metadata.Browse.Requests; +namespace Kavita.Models.DTOs.Metadata.Browse.Requests; #nullable enable public sealed record BrowsePersonFilterDto diff --git a/API/DTOs/Metadata/ChapterMetadataDto.cs b/Kavita.Models/DTOs/Metadata/ChapterMetadataDto.cs similarity index 95% rename from API/DTOs/Metadata/ChapterMetadataDto.cs rename to Kavita.Models/DTOs/Metadata/ChapterMetadataDto.cs index c79436e24..8c06a18c4 100644 --- a/API/DTOs/Metadata/ChapterMetadataDto.cs +++ b/Kavita.Models/DTOs/Metadata/ChapterMetadataDto.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; -using API.DTOs.Person; -using API.Entities.Enums; +using Kavita.Models.DTOs.Person; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Metadata; +namespace Kavita.Models.DTOs.Metadata; #nullable enable /// diff --git a/API/DTOs/Metadata/GenreTagDto.cs b/Kavita.Models/DTOs/Metadata/GenreTagDto.cs similarity index 72% rename from API/DTOs/Metadata/GenreTagDto.cs rename to Kavita.Models/DTOs/Metadata/GenreTagDto.cs index 13a339d38..a4cf3c34a 100644 --- a/API/DTOs/Metadata/GenreTagDto.cs +++ b/Kavita.Models/DTOs/Metadata/GenreTagDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Metadata; +namespace Kavita.Models.DTOs.Metadata; public record GenreTagDto { diff --git a/API/DTOs/Metadata/Matching/ExternalSeriesMatchDto.cs b/Kavita.Models/DTOs/Metadata/Matching/ExternalSeriesMatchDto.cs similarity index 61% rename from API/DTOs/Metadata/Matching/ExternalSeriesMatchDto.cs rename to Kavita.Models/DTOs/Metadata/Matching/ExternalSeriesMatchDto.cs index 209d1b4d6..a19738985 100644 --- a/API/DTOs/Metadata/Matching/ExternalSeriesMatchDto.cs +++ b/Kavita.Models/DTOs/Metadata/Matching/ExternalSeriesMatchDto.cs @@ -1,6 +1,6 @@ -using API.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.KavitaPlus.Metadata; -namespace API.DTOs.Metadata.Matching; +namespace Kavita.Models.DTOs.Metadata.Matching; public sealed record ExternalSeriesMatchDto { diff --git a/API/DTOs/Metadata/Matching/MatchSeriesDto.cs b/Kavita.Models/DTOs/Metadata/Matching/MatchSeriesDto.cs similarity index 92% rename from API/DTOs/Metadata/Matching/MatchSeriesDto.cs rename to Kavita.Models/DTOs/Metadata/Matching/MatchSeriesDto.cs index bb497b9ab..5ad9e166d 100644 --- a/API/DTOs/Metadata/Matching/MatchSeriesDto.cs +++ b/Kavita.Models/DTOs/Metadata/Matching/MatchSeriesDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Metadata.Matching; +namespace Kavita.Models.DTOs.Metadata.Matching; /// /// Used for matching a series with Kavita+ for metadata and scrobbling diff --git a/API/DTOs/Metadata/PublicationStatusDto.cs b/Kavita.Models/DTOs/Metadata/PublicationStatusDto.cs similarity index 64% rename from API/DTOs/Metadata/PublicationStatusDto.cs rename to Kavita.Models/DTOs/Metadata/PublicationStatusDto.cs index b4f12500a..94b65df3a 100644 --- a/API/DTOs/Metadata/PublicationStatusDto.cs +++ b/Kavita.Models/DTOs/Metadata/PublicationStatusDto.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Metadata; +namespace Kavita.Models.DTOs.Metadata; public sealed record PublicationStatusDto { diff --git a/API/DTOs/Metadata/TagDto.cs b/Kavita.Models/DTOs/Metadata/TagDto.cs similarity index 71% rename from API/DTOs/Metadata/TagDto.cs rename to Kavita.Models/DTOs/Metadata/TagDto.cs index f5c925e1f..c0c2fbe8d 100644 --- a/API/DTOs/Metadata/TagDto.cs +++ b/Kavita.Models/DTOs/Metadata/TagDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Metadata; +namespace Kavita.Models.DTOs.Metadata; public record TagDto { diff --git a/API/DTOs/Misc/ParseBulkRequestDto.cs b/Kavita.Models/DTOs/Misc/ParseBulkRequestDto.cs similarity index 71% rename from API/DTOs/Misc/ParseBulkRequestDto.cs rename to Kavita.Models/DTOs/Misc/ParseBulkRequestDto.cs index 7e529e9ed..9b7a4537c 100644 --- a/API/DTOs/Misc/ParseBulkRequestDto.cs +++ b/Kavita.Models/DTOs/Misc/ParseBulkRequestDto.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Misc; +namespace Kavita.Models.DTOs.Misc; public sealed record ParseBulkRequestDto { diff --git a/API/DTOs/Misc/ParseBulkResponseDto.cs b/Kavita.Models/DTOs/Misc/ParseBulkResponseDto.cs similarity index 94% rename from API/DTOs/Misc/ParseBulkResponseDto.cs rename to Kavita.Models/DTOs/Misc/ParseBulkResponseDto.cs index c46f1b78b..6e7f1172f 100644 --- a/API/DTOs/Misc/ParseBulkResponseDto.cs +++ b/Kavita.Models/DTOs/Misc/ParseBulkResponseDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Misc; +namespace Kavita.Models.DTOs.Misc; public record ParseBulkResponseDto { diff --git a/API/DTOs/Misc/ParseResultDto.cs b/Kavita.Models/DTOs/Misc/ParseResultDto.cs similarity index 90% rename from API/DTOs/Misc/ParseResultDto.cs rename to Kavita.Models/DTOs/Misc/ParseResultDto.cs index dd2b8771e..0a8d55299 100644 --- a/API/DTOs/Misc/ParseResultDto.cs +++ b/Kavita.Models/DTOs/Misc/ParseResultDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.Misc; +namespace Kavita.Models.DTOs.Misc; public sealed record ParseResultDto { diff --git a/API/DTOs/OPDS/Internal/Feed.cs b/Kavita.Models/DTOs/OPDS/Internal/Feed.cs similarity index 96% rename from API/DTOs/OPDS/Internal/Feed.cs rename to Kavita.Models/DTOs/OPDS/Internal/Feed.cs index 5663e8200..4be3a7f2f 100644 --- a/API/DTOs/OPDS/Internal/Feed.cs +++ b/Kavita.Models/DTOs/OPDS/Internal/Feed.cs @@ -1,9 +1,8 @@ using System; using System.Collections.Generic; using System.Xml.Serialization; -using Newtonsoft.Json; -namespace API.DTOs.OPDS; +namespace Kavita.Models.DTOs.OPDS; [XmlRoot("feed", Namespace = "http://www.w3.org/2005/Atom")] public sealed record Feed diff --git a/API/DTOs/OPDS/Internal/FeedAuthor.cs b/Kavita.Models/DTOs/OPDS/Internal/FeedAuthor.cs similarity index 84% rename from API/DTOs/OPDS/Internal/FeedAuthor.cs rename to Kavita.Models/DTOs/OPDS/Internal/FeedAuthor.cs index 4196997dd..71d2e7f55 100644 --- a/API/DTOs/OPDS/Internal/FeedAuthor.cs +++ b/Kavita.Models/DTOs/OPDS/Internal/FeedAuthor.cs @@ -1,6 +1,6 @@ using System.Xml.Serialization; -namespace API.DTOs.OPDS; +namespace Kavita.Models.DTOs.OPDS; public sealed record FeedAuthor { diff --git a/API/DTOs/OPDS/Internal/FeedCategory.cs b/Kavita.Models/DTOs/OPDS/Internal/FeedCategory.cs similarity index 92% rename from API/DTOs/OPDS/Internal/FeedCategory.cs rename to Kavita.Models/DTOs/OPDS/Internal/FeedCategory.cs index 2352b4af2..e20b5f9fe 100644 --- a/API/DTOs/OPDS/Internal/FeedCategory.cs +++ b/Kavita.Models/DTOs/OPDS/Internal/FeedCategory.cs @@ -1,6 +1,6 @@ using System.Xml.Serialization; -namespace API.DTOs.OPDS; +namespace Kavita.Models.DTOs.OPDS; public sealed record FeedCategory { diff --git a/API/DTOs/OPDS/Internal/FeedEntry.cs b/Kavita.Models/DTOs/OPDS/Internal/FeedEntry.cs similarity index 97% rename from API/DTOs/OPDS/Internal/FeedEntry.cs rename to Kavita.Models/DTOs/OPDS/Internal/FeedEntry.cs index 838ebd124..b9c44380e 100644 --- a/API/DTOs/OPDS/Internal/FeedEntry.cs +++ b/Kavita.Models/DTOs/OPDS/Internal/FeedEntry.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Xml.Serialization; -namespace API.DTOs.OPDS; +namespace Kavita.Models.DTOs.OPDS; #nullable enable public sealed record FeedEntry diff --git a/API/DTOs/OPDS/Internal/FeedEntryContent.cs b/Kavita.Models/DTOs/OPDS/Internal/FeedEntryContent.cs similarity index 83% rename from API/DTOs/OPDS/Internal/FeedEntryContent.cs rename to Kavita.Models/DTOs/OPDS/Internal/FeedEntryContent.cs index 4de9b73bd..a65184834 100644 --- a/API/DTOs/OPDS/Internal/FeedEntryContent.cs +++ b/Kavita.Models/DTOs/OPDS/Internal/FeedEntryContent.cs @@ -1,6 +1,6 @@ using System.Xml.Serialization; -namespace API.DTOs.OPDS; +namespace Kavita.Models.DTOs.OPDS; public sealed record FeedEntryContent { diff --git a/API/DTOs/OPDS/Internal/FeedLink.cs b/Kavita.Models/DTOs/OPDS/Internal/FeedLink.cs similarity index 97% rename from API/DTOs/OPDS/Internal/FeedLink.cs rename to Kavita.Models/DTOs/OPDS/Internal/FeedLink.cs index 95d65f907..e0eaabf55 100644 --- a/API/DTOs/OPDS/Internal/FeedLink.cs +++ b/Kavita.Models/DTOs/OPDS/Internal/FeedLink.cs @@ -1,6 +1,6 @@ using System.Xml.Serialization; -namespace API.DTOs.OPDS; +namespace Kavita.Models.DTOs.OPDS; public sealed record FeedLink { diff --git a/API/DTOs/OPDS/Internal/FeedLinkRelation.cs b/Kavita.Models/DTOs/OPDS/Internal/FeedLinkRelation.cs similarity index 95% rename from API/DTOs/OPDS/Internal/FeedLinkRelation.cs rename to Kavita.Models/DTOs/OPDS/Internal/FeedLinkRelation.cs index 4c9ee2c94..903bf5bdd 100644 --- a/API/DTOs/OPDS/Internal/FeedLinkRelation.cs +++ b/Kavita.Models/DTOs/OPDS/Internal/FeedLinkRelation.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.OPDS; +namespace Kavita.Models.DTOs.OPDS; public static class FeedLinkRelation { diff --git a/API/DTOs/OPDS/Internal/FeedLinkType.cs b/Kavita.Models/DTOs/OPDS/Internal/FeedLinkType.cs similarity index 91% rename from API/DTOs/OPDS/Internal/FeedLinkType.cs rename to Kavita.Models/DTOs/OPDS/Internal/FeedLinkType.cs index 6ae48bd52..4bb0cde7d 100644 --- a/API/DTOs/OPDS/Internal/FeedLinkType.cs +++ b/Kavita.Models/DTOs/OPDS/Internal/FeedLinkType.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.OPDS; +namespace Kavita.Models.DTOs.OPDS; public static class FeedLinkType { diff --git a/API/DTOs/OPDS/Internal/OpenSearchDescription.cs b/Kavita.Models/DTOs/OPDS/Internal/OpenSearchDescription.cs similarity index 98% rename from API/DTOs/OPDS/Internal/OpenSearchDescription.cs rename to Kavita.Models/DTOs/OPDS/Internal/OpenSearchDescription.cs index eba26572f..ff3d9eb2c 100644 --- a/API/DTOs/OPDS/Internal/OpenSearchDescription.cs +++ b/Kavita.Models/DTOs/OPDS/Internal/OpenSearchDescription.cs @@ -1,6 +1,6 @@ using System.Xml.Serialization; -namespace API.DTOs.OPDS; +namespace Kavita.Models.DTOs.OPDS; [XmlRoot("OpenSearchDescription", Namespace = "http://a9.com/-/spec/opensearch/1.1/")] public sealed record OpenSearchDescription diff --git a/API/DTOs/OPDS/Internal/SearchLink.cs b/Kavita.Models/DTOs/OPDS/Internal/SearchLink.cs similarity index 89% rename from API/DTOs/OPDS/Internal/SearchLink.cs rename to Kavita.Models/DTOs/OPDS/Internal/SearchLink.cs index b4698c221..4673f8de8 100644 --- a/API/DTOs/OPDS/Internal/SearchLink.cs +++ b/Kavita.Models/DTOs/OPDS/Internal/SearchLink.cs @@ -1,6 +1,6 @@ using System.Xml.Serialization; -namespace API.DTOs.OPDS; +namespace Kavita.Models.DTOs.OPDS; public sealed record SearchLink { diff --git a/API/DTOs/OPDS/Requests/IOpdsPagination.cs b/Kavita.Models/DTOs/OPDS/Requests/IOpdsPagination.cs similarity index 62% rename from API/DTOs/OPDS/Requests/IOpdsPagination.cs rename to Kavita.Models/DTOs/OPDS/Requests/IOpdsPagination.cs index 7dba9cc8d..ac0644161 100644 --- a/API/DTOs/OPDS/Requests/IOpdsPagination.cs +++ b/Kavita.Models/DTOs/OPDS/Requests/IOpdsPagination.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.OPDS.Requests; +namespace Kavita.Models.DTOs.OPDS.Requests; public interface IOpdsPagination { diff --git a/API/DTOs/OPDS/Requests/IOpdsRequest.cs b/Kavita.Models/DTOs/OPDS/Requests/IOpdsRequest.cs similarity index 72% rename from API/DTOs/OPDS/Requests/IOpdsRequest.cs rename to Kavita.Models/DTOs/OPDS/Requests/IOpdsRequest.cs index 201431741..f66ca8582 100644 --- a/API/DTOs/OPDS/Requests/IOpdsRequest.cs +++ b/Kavita.Models/DTOs/OPDS/Requests/IOpdsRequest.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.Enums.UserPreferences; -namespace API.DTOs.OPDS.Requests; +namespace Kavita.Models.DTOs.OPDS.Requests; public interface IOpdsRequest { diff --git a/API/DTOs/OPDS/Requests/OpdsCatalogeRequest.cs b/Kavita.Models/DTOs/OPDS/Requests/OpdsCatalogeRequest.cs similarity index 74% rename from API/DTOs/OPDS/Requests/OpdsCatalogeRequest.cs rename to Kavita.Models/DTOs/OPDS/Requests/OpdsCatalogeRequest.cs index 5c0f9ffe9..890f4cf4c 100644 --- a/API/DTOs/OPDS/Requests/OpdsCatalogeRequest.cs +++ b/Kavita.Models/DTOs/OPDS/Requests/OpdsCatalogeRequest.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.Enums.UserPreferences; -namespace API.DTOs.OPDS.Requests; +namespace Kavita.Models.DTOs.OPDS.Requests; public sealed record OpdsCatalogueRequest : IOpdsRequest diff --git a/API/DTOs/OPDS/Requests/OpdsItemsFromCompoundEntityIdsRequest.cs b/Kavita.Models/DTOs/OPDS/Requests/OpdsItemsFromCompoundEntityIdsRequest.cs similarity index 87% rename from API/DTOs/OPDS/Requests/OpdsItemsFromCompoundEntityIdsRequest.cs rename to Kavita.Models/DTOs/OPDS/Requests/OpdsItemsFromCompoundEntityIdsRequest.cs index 3739bea4c..3434a7c22 100644 --- a/API/DTOs/OPDS/Requests/OpdsItemsFromCompoundEntityIdsRequest.cs +++ b/Kavita.Models/DTOs/OPDS/Requests/OpdsItemsFromCompoundEntityIdsRequest.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.Enums.UserPreferences; -namespace API.DTOs.OPDS.Requests; +namespace Kavita.Models.DTOs.OPDS.Requests; /// /// A special case for dealing with lower level entities (volume/chapter) which need higher level entity ids diff --git a/API/DTOs/OPDS/Requests/OpdsSearchRequest.cs b/Kavita.Models/DTOs/OPDS/Requests/OpdsSearchRequest.cs similarity index 76% rename from API/DTOs/OPDS/Requests/OpdsSearchRequest.cs rename to Kavita.Models/DTOs/OPDS/Requests/OpdsSearchRequest.cs index bcbe4d5bf..a59f141b7 100644 --- a/API/DTOs/OPDS/Requests/OpdsSearchRequest.cs +++ b/Kavita.Models/DTOs/OPDS/Requests/OpdsSearchRequest.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.Enums.UserPreferences; -namespace API.DTOs.OPDS.Requests; +namespace Kavita.Models.DTOs.OPDS.Requests; public sealed record OpdsSearchRequest : IOpdsRequest { diff --git a/API/DTOs/OPDS/Requests/OpdsSmartFilterCatalogueRequest.cs b/Kavita.Models/DTOs/OPDS/Requests/OpdsSmartFilterCatalogueRequest.cs similarity index 81% rename from API/DTOs/OPDS/Requests/OpdsSmartFilterCatalogueRequest.cs rename to Kavita.Models/DTOs/OPDS/Requests/OpdsSmartFilterCatalogueRequest.cs index f5e73a7bb..1e9848344 100644 --- a/API/DTOs/OPDS/Requests/OpdsSmartFilterCatalogueRequest.cs +++ b/Kavita.Models/DTOs/OPDS/Requests/OpdsSmartFilterCatalogueRequest.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.Enums.UserPreferences; -namespace API.DTOs.OPDS.Requests; +namespace Kavita.Models.DTOs.OPDS.Requests; /// /// A generic Catalogue request for a specific Entity diff --git a/API/DTOs/OPDS/Requests/OpdsSmartFilterRequest.cs b/Kavita.Models/DTOs/OPDS/Requests/OpdsSmartFilterRequest.cs similarity index 79% rename from API/DTOs/OPDS/Requests/OpdsSmartFilterRequest.cs rename to Kavita.Models/DTOs/OPDS/Requests/OpdsSmartFilterRequest.cs index cb33bbe9b..2aa9c3200 100644 --- a/API/DTOs/OPDS/Requests/OpdsSmartFilterRequest.cs +++ b/Kavita.Models/DTOs/OPDS/Requests/OpdsSmartFilterRequest.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.Enums.UserPreferences; -namespace API.DTOs.OPDS.Requests; +namespace Kavita.Models.DTOs.OPDS.Requests; public sealed record OpdsItemsFromEntityIdRequest : IOpdsRequest, IOpdsPagination { diff --git a/API/DTOs/Person/PersonAliasCheckDto.cs b/Kavita.Models/DTOs/Person/PersonAliasCheckDto.cs similarity index 94% rename from API/DTOs/Person/PersonAliasCheckDto.cs rename to Kavita.Models/DTOs/Person/PersonAliasCheckDto.cs index f0f09a7d4..0c4458c59 100644 --- a/API/DTOs/Person/PersonAliasCheckDto.cs +++ b/Kavita.Models/DTOs/Person/PersonAliasCheckDto.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record PersonAliasCheckDto { diff --git a/API/DTOs/Person/PersonDto.cs b/Kavita.Models/DTOs/Person/PersonDto.cs similarity index 95% rename from API/DTOs/Person/PersonDto.cs rename to Kavita.Models/DTOs/Person/PersonDto.cs index 2969561cc..807e694aa 100644 --- a/API/DTOs/Person/PersonDto.cs +++ b/Kavita.Models/DTOs/Person/PersonDto.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Person; +namespace Kavita.Models.DTOs.Person; #nullable enable public class PersonDto diff --git a/API/DTOs/Person/PersonMergeDto.cs b/Kavita.Models/DTOs/Person/PersonMergeDto.cs similarity index 93% rename from API/DTOs/Person/PersonMergeDto.cs rename to Kavita.Models/DTOs/Person/PersonMergeDto.cs index b5dc23375..fec3cfdbf 100644 --- a/API/DTOs/Person/PersonMergeDto.cs +++ b/Kavita.Models/DTOs/Person/PersonMergeDto.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record PersonMergeDto { diff --git a/API/DTOs/Person/UpdatePersonDto.cs b/Kavita.Models/DTOs/Person/UpdatePersonDto.cs similarity index 94% rename from API/DTOs/Person/UpdatePersonDto.cs rename to Kavita.Models/DTOs/Person/UpdatePersonDto.cs index b43a45e88..ce136c2d3 100644 --- a/API/DTOs/Person/UpdatePersonDto.cs +++ b/Kavita.Models/DTOs/Person/UpdatePersonDto.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable public sealed record UpdatePersonDto diff --git a/API/DTOs/Progress/ClientDeviceDto.cs b/Kavita.Models/DTOs/Progress/ClientDeviceDto.cs similarity index 94% rename from API/DTOs/Progress/ClientDeviceDto.cs rename to Kavita.Models/DTOs/Progress/ClientDeviceDto.cs index cce9c056d..8b23c16da 100644 --- a/API/DTOs/Progress/ClientDeviceDto.cs +++ b/Kavita.Models/DTOs/Progress/ClientDeviceDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.Progress; +namespace Kavita.Models.DTOs.Progress; public sealed record ClientDeviceDto { diff --git a/API/DTOs/Progress/ClientInfoDto.cs b/Kavita.Models/DTOs/Progress/ClientInfoDto.cs similarity index 94% rename from API/DTOs/Progress/ClientInfoDto.cs rename to Kavita.Models/DTOs/Progress/ClientInfoDto.cs index b354b034a..342e261b3 100644 --- a/API/DTOs/Progress/ClientInfoDto.cs +++ b/Kavita.Models/DTOs/Progress/ClientInfoDto.cs @@ -1,8 +1,7 @@ -using API.Constants; -using API.Entities.Enums; -using API.Entities.Progress; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; -namespace API.DTOs.Progress; +namespace Kavita.Models.DTOs.Progress; #nullable enable public sealed record ClientInfoDto diff --git a/API/DTOs/Progress/DailyReadingDataDto.cs b/Kavita.Models/DTOs/Progress/DailyReadingDataDto.cs similarity index 94% rename from API/DTOs/Progress/DailyReadingDataDto.cs rename to Kavita.Models/DTOs/Progress/DailyReadingDataDto.cs index d742503cd..a0df959e9 100644 --- a/API/DTOs/Progress/DailyReadingDataDto.cs +++ b/Kavita.Models/DTOs/Progress/DailyReadingDataDto.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; -using API.Entities; -using API.Entities.Enums; -using API.Services; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Progress; +namespace Kavita.Models.DTOs.Progress; #nullable enable public class DailyReadingDataDto diff --git a/API/DTOs/Progress/FullProgressDto.cs b/Kavita.Models/DTOs/Progress/FullProgressDto.cs similarity index 93% rename from API/DTOs/Progress/FullProgressDto.cs rename to Kavita.Models/DTOs/Progress/FullProgressDto.cs index 4f97ab44a..d623b392e 100644 --- a/API/DTOs/Progress/FullProgressDto.cs +++ b/Kavita.Models/DTOs/Progress/FullProgressDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.Progress; +namespace Kavita.Models.DTOs.Progress; /// /// A full progress Record from the DB (not all data, only what's needed for API) diff --git a/API/DTOs/Progress/ProgressDto.cs b/Kavita.Models/DTOs/Progress/ProgressDto.cs similarity index 93% rename from API/DTOs/Progress/ProgressDto.cs rename to Kavita.Models/DTOs/Progress/ProgressDto.cs index bf82fff35..78ac7b159 100644 --- a/API/DTOs/Progress/ProgressDto.cs +++ b/Kavita.Models/DTOs/Progress/ProgressDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.Progress; +namespace Kavita.Models.DTOs.Progress; #nullable enable public sealed record ProgressDto diff --git a/API/DTOs/Progress/ReadingActivityDataDto.cs b/Kavita.Models/DTOs/Progress/ReadingActivityDataDto.cs similarity index 95% rename from API/DTOs/Progress/ReadingActivityDataDto.cs rename to Kavita.Models/DTOs/Progress/ReadingActivityDataDto.cs index fc5666a9c..dee47054c 100644 --- a/API/DTOs/Progress/ReadingActivityDataDto.cs +++ b/Kavita.Models/DTOs/Progress/ReadingActivityDataDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.Progress; +namespace Kavita.Models.DTOs.Progress; #nullable enable public sealed record ReadingActivityDataDto diff --git a/API/DTOs/Progress/ReadingSessionDto.cs b/Kavita.Models/DTOs/Progress/ReadingSessionDto.cs similarity index 91% rename from API/DTOs/Progress/ReadingSessionDto.cs rename to Kavita.Models/DTOs/Progress/ReadingSessionDto.cs index ec39b1e07..9ef944e79 100644 --- a/API/DTOs/Progress/ReadingSessionDto.cs +++ b/Kavita.Models/DTOs/Progress/ReadingSessionDto.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace API.DTOs.Progress; +namespace Kavita.Models.DTOs.Progress; public sealed record ReadingSessionDto { diff --git a/API/DTOs/RatingDto.cs b/Kavita.Models/DTOs/RatingDto.cs similarity index 77% rename from API/DTOs/RatingDto.cs rename to Kavita.Models/DTOs/RatingDto.cs index c22e898c2..fe0bdcff8 100644 --- a/API/DTOs/RatingDto.cs +++ b/Kavita.Models/DTOs/RatingDto.cs @@ -1,8 +1,7 @@ -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Services.Plus; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable public sealed record RatingDto diff --git a/API/DTOs/Reader/AnnotationDto.cs b/Kavita.Models/DTOs/Reader/AnnotationDto.cs similarity index 94% rename from API/DTOs/Reader/AnnotationDto.cs rename to Kavita.Models/DTOs/Reader/AnnotationDto.cs index 18cb563ea..33f0a0da0 100644 --- a/API/DTOs/Reader/AnnotationDto.cs +++ b/Kavita.Models/DTOs/Reader/AnnotationDto.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; -using API.Entities; -using API.Entities.Enums; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; /// /// Represents an annotation on a book diff --git a/API/DTOs/Reader/BookChapterItem.cs b/Kavita.Models/DTOs/Reader/BookChapterItem.cs similarity index 93% rename from API/DTOs/Reader/BookChapterItem.cs rename to Kavita.Models/DTOs/Reader/BookChapterItem.cs index 892e82e27..4523de198 100644 --- a/API/DTOs/Reader/BookChapterItem.cs +++ b/Kavita.Models/DTOs/Reader/BookChapterItem.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; public sealed record BookChapterItem { diff --git a/API/DTOs/Reader/BookInfoDto.cs b/Kavita.Models/DTOs/Reader/BookInfoDto.cs similarity index 88% rename from API/DTOs/Reader/BookInfoDto.cs rename to Kavita.Models/DTOs/Reader/BookInfoDto.cs index 2473cd5dc..ca6584956 100644 --- a/API/DTOs/Reader/BookInfoDto.cs +++ b/Kavita.Models/DTOs/Reader/BookInfoDto.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; public sealed record BookInfoDto : IChapterInfoDto { diff --git a/API/DTOs/Reader/BookResourceResultDto.cs b/Kavita.Models/DTOs/Reader/BookResourceResultDto.cs similarity index 93% rename from API/DTOs/Reader/BookResourceResultDto.cs rename to Kavita.Models/DTOs/Reader/BookResourceResultDto.cs index 9935341d9..7d89aa43b 100644 --- a/API/DTOs/Reader/BookResourceResultDto.cs +++ b/Kavita.Models/DTOs/Reader/BookResourceResultDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; public sealed record BookResourceResultDto { diff --git a/API/DTOs/Reader/BookmarkDto.cs b/Kavita.Models/DTOs/Reader/BookmarkDto.cs similarity index 95% rename from API/DTOs/Reader/BookmarkDto.cs rename to Kavita.Models/DTOs/Reader/BookmarkDto.cs index b62271408..9537826da 100644 --- a/API/DTOs/Reader/BookmarkDto.cs +++ b/Kavita.Models/DTOs/Reader/BookmarkDto.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; #nullable enable public sealed record BookmarkDto diff --git a/API/DTOs/Reader/BookmarkInfoDto.cs b/Kavita.Models/DTOs/Reader/BookmarkInfoDto.cs similarity index 92% rename from API/DTOs/Reader/BookmarkInfoDto.cs rename to Kavita.Models/DTOs/Reader/BookmarkInfoDto.cs index c75c3d8bf..62fe20c94 100644 --- a/API/DTOs/Reader/BookmarkInfoDto.cs +++ b/Kavita.Models/DTOs/Reader/BookmarkInfoDto.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; #nullable enable public class BookmarkInfoDto diff --git a/API/DTOs/Reader/BulkRemoveBookmarkForSeriesDto.cs b/Kavita.Models/DTOs/Reader/BulkRemoveBookmarkForSeriesDto.cs similarity index 81% rename from API/DTOs/Reader/BulkRemoveBookmarkForSeriesDto.cs rename to Kavita.Models/DTOs/Reader/BulkRemoveBookmarkForSeriesDto.cs index 51ccf5cc3..d862b2fc7 100644 --- a/API/DTOs/Reader/BulkRemoveBookmarkForSeriesDto.cs +++ b/Kavita.Models/DTOs/Reader/BulkRemoveBookmarkForSeriesDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; public sealed record BulkRemoveBookmarkForSeriesDto { diff --git a/API/DTOs/Reader/ChapterInfoDto.cs b/Kavita.Models/DTOs/Reader/ChapterInfoDto.cs similarity index 97% rename from API/DTOs/Reader/ChapterInfoDto.cs rename to Kavita.Models/DTOs/Reader/ChapterInfoDto.cs index 4da08a31d..ca3a9602e 100644 --- a/API/DTOs/Reader/ChapterInfoDto.cs +++ b/Kavita.Models/DTOs/Reader/ChapterInfoDto.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; #nullable enable /// diff --git a/API/DTOs/Reader/CreatePersonalToCDto.cs b/Kavita.Models/DTOs/Reader/CreatePersonalToCDto.cs similarity index 91% rename from API/DTOs/Reader/CreatePersonalToCDto.cs rename to Kavita.Models/DTOs/Reader/CreatePersonalToCDto.cs index 545e17e47..ef66f86a1 100644 --- a/API/DTOs/Reader/CreatePersonalToCDto.cs +++ b/Kavita.Models/DTOs/Reader/CreatePersonalToCDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; #nullable enable public sealed record CreatePersonalToCDto diff --git a/API/DTOs/Reader/FileDimensionDto.cs b/Kavita.Models/DTOs/Reader/FileDimensionDto.cs similarity index 91% rename from API/DTOs/Reader/FileDimensionDto.cs rename to Kavita.Models/DTOs/Reader/FileDimensionDto.cs index 7a7d2978f..97bfd7692 100644 --- a/API/DTOs/Reader/FileDimensionDto.cs +++ b/Kavita.Models/DTOs/Reader/FileDimensionDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; public sealed record FileDimensionDto { diff --git a/API/DTOs/Reader/HourEstimateRangeDto.cs b/Kavita.Models/DTOs/Reader/HourEstimateRangeDto.cs similarity index 92% rename from API/DTOs/Reader/HourEstimateRangeDto.cs rename to Kavita.Models/DTOs/Reader/HourEstimateRangeDto.cs index 3facf8e56..c2baeb25d 100644 --- a/API/DTOs/Reader/HourEstimateRangeDto.cs +++ b/Kavita.Models/DTOs/Reader/HourEstimateRangeDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; /// /// A range of time to read a selection (series, chapter, etc) diff --git a/API/DTOs/Reader/IChapterInfoDto.cs b/Kavita.Models/DTOs/Reader/IChapterInfoDto.cs similarity index 85% rename from API/DTOs/Reader/IChapterInfoDto.cs rename to Kavita.Models/DTOs/Reader/IChapterInfoDto.cs index 6a9a74a2c..ad3523070 100644 --- a/API/DTOs/Reader/IChapterInfoDto.cs +++ b/Kavita.Models/DTOs/Reader/IChapterInfoDto.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; public interface IChapterInfoDto { diff --git a/API/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs b/Kavita.Models/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs similarity index 81% rename from API/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs rename to Kavita.Models/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs index 4c39f7d76..70fe4c685 100644 --- a/API/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs +++ b/Kavita.Models/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; public sealed record MarkMultipleSeriesAsReadDto { diff --git a/API/DTOs/Reader/MarkReadDto.cs b/Kavita.Models/DTOs/Reader/MarkReadDto.cs similarity index 65% rename from API/DTOs/Reader/MarkReadDto.cs rename to Kavita.Models/DTOs/Reader/MarkReadDto.cs index c6f7367c0..e0750bcd2 100644 --- a/API/DTOs/Reader/MarkReadDto.cs +++ b/Kavita.Models/DTOs/Reader/MarkReadDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; public sealed record MarkReadDto { diff --git a/API/DTOs/Reader/MarkVolumeReadDto.cs b/Kavita.Models/DTOs/Reader/MarkVolumeReadDto.cs similarity index 75% rename from API/DTOs/Reader/MarkVolumeReadDto.cs rename to Kavita.Models/DTOs/Reader/MarkVolumeReadDto.cs index be95d2e98..00f05ddcd 100644 --- a/API/DTOs/Reader/MarkVolumeReadDto.cs +++ b/Kavita.Models/DTOs/Reader/MarkVolumeReadDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; public sealed record MarkVolumeReadDto { diff --git a/API/DTOs/Reader/MarkVolumesReadDto.cs b/Kavita.Models/DTOs/Reader/MarkVolumesReadDto.cs similarity index 93% rename from API/DTOs/Reader/MarkVolumesReadDto.cs rename to Kavita.Models/DTOs/Reader/MarkVolumesReadDto.cs index b07bfbc67..b721c72f3 100644 --- a/API/DTOs/Reader/MarkVolumesReadDto.cs +++ b/Kavita.Models/DTOs/Reader/MarkVolumesReadDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; /// /// This is used for bulk updating a set of volume and or chapters in one go diff --git a/API/DTOs/Reader/PersonalToCDto.cs b/Kavita.Models/DTOs/Reader/PersonalToCDto.cs similarity index 96% rename from API/DTOs/Reader/PersonalToCDto.cs rename to Kavita.Models/DTOs/Reader/PersonalToCDto.cs index 66994a7ff..c6ab2b032 100644 --- a/API/DTOs/Reader/PersonalToCDto.cs +++ b/Kavita.Models/DTOs/Reader/PersonalToCDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; #nullable enable diff --git a/API/DTOs/Reader/ReReadDto.cs b/Kavita.Models/DTOs/Reader/ReReadDto.cs similarity index 94% rename from API/DTOs/Reader/ReReadDto.cs rename to Kavita.Models/DTOs/Reader/ReReadDto.cs index 97e746a9c..2943fabc1 100644 --- a/API/DTOs/Reader/ReReadDto.cs +++ b/Kavita.Models/DTOs/Reader/ReReadDto.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; public sealed record RereadDto { diff --git a/API/DTOs/Reader/RemoveBookmarkForSeriesDto.cs b/Kavita.Models/DTOs/Reader/RemoveBookmarkForSeriesDto.cs similarity index 69% rename from API/DTOs/Reader/RemoveBookmarkForSeriesDto.cs rename to Kavita.Models/DTOs/Reader/RemoveBookmarkForSeriesDto.cs index ecbb744c8..1a7c2f4dd 100644 --- a/API/DTOs/Reader/RemoveBookmarkForSeriesDto.cs +++ b/Kavita.Models/DTOs/Reader/RemoveBookmarkForSeriesDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Reader; +namespace Kavita.Models.DTOs.Reader; public sealed record RemoveBookmarkForSeriesDto { diff --git a/API/DTOs/ReadingLists/CBL/CblBook.cs b/Kavita.Models/DTOs/ReadingLists/CBL/CblBook.cs similarity index 94% rename from API/DTOs/ReadingLists/CBL/CblBook.cs rename to Kavita.Models/DTOs/ReadingLists/CBL/CblBook.cs index d51795b8d..177423371 100644 --- a/API/DTOs/ReadingLists/CBL/CblBook.cs +++ b/Kavita.Models/DTOs/ReadingLists/CBL/CblBook.cs @@ -1,7 +1,6 @@ using System.Xml.Serialization; -using API.Data.Metadata; -namespace API.DTOs.ReadingLists.CBL; +namespace Kavita.Models.DTOs.ReadingLists.CBL; [XmlRoot(ElementName="Book")] diff --git a/API/DTOs/ReadingLists/CBL/CblConflictsDto.cs b/Kavita.Models/DTOs/ReadingLists/CBL/CblConflictsDto.cs similarity index 79% rename from API/DTOs/ReadingLists/CBL/CblConflictsDto.cs rename to Kavita.Models/DTOs/ReadingLists/CBL/CblConflictsDto.cs index 35234923f..a90b52988 100644 --- a/API/DTOs/ReadingLists/CBL/CblConflictsDto.cs +++ b/Kavita.Models/DTOs/ReadingLists/CBL/CblConflictsDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.ReadingLists.CBL; +namespace Kavita.Models.DTOs.ReadingLists.CBL; public sealed record CblConflictQuestion diff --git a/API/DTOs/ReadingLists/CBL/CblImportSummary.cs b/Kavita.Models/DTOs/ReadingLists/CBL/CblImportSummary.cs similarity index 98% rename from API/DTOs/ReadingLists/CBL/CblImportSummary.cs rename to Kavita.Models/DTOs/ReadingLists/CBL/CblImportSummary.cs index b9716421e..3315837c1 100644 --- a/API/DTOs/ReadingLists/CBL/CblImportSummary.cs +++ b/Kavita.Models/DTOs/ReadingLists/CBL/CblImportSummary.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.ComponentModel; -namespace API.DTOs.ReadingLists.CBL; +namespace Kavita.Models.DTOs.ReadingLists.CBL; public enum CblImportResult { /// diff --git a/API/DTOs/ReadingLists/CBL/CblReadingList.cs b/Kavita.Models/DTOs/ReadingLists/CBL/CblReadingList.cs similarity index 97% rename from API/DTOs/ReadingLists/CBL/CblReadingList.cs rename to Kavita.Models/DTOs/ReadingLists/CBL/CblReadingList.cs index 15b349f42..aa368f09d 100644 --- a/API/DTOs/ReadingLists/CBL/CblReadingList.cs +++ b/Kavita.Models/DTOs/ReadingLists/CBL/CblReadingList.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Xml.Serialization; -namespace API.DTOs.ReadingLists.CBL; +namespace Kavita.Models.DTOs.ReadingLists.CBL; [XmlRoot(ElementName="Books")] diff --git a/API/DTOs/ReadingLists/CreateReadingListDto.cs b/Kavita.Models/DTOs/ReadingLists/CreateReadingListDto.cs similarity index 68% rename from API/DTOs/ReadingLists/CreateReadingListDto.cs rename to Kavita.Models/DTOs/ReadingLists/CreateReadingListDto.cs index 543215722..35de4fbda 100644 --- a/API/DTOs/ReadingLists/CreateReadingListDto.cs +++ b/Kavita.Models/DTOs/ReadingLists/CreateReadingListDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; public sealed record CreateReadingListDto { diff --git a/API/DTOs/ReadingLists/DeleteReadingListsDto.cs b/Kavita.Models/DTOs/ReadingLists/DeleteReadingListsDto.cs similarity index 82% rename from API/DTOs/ReadingLists/DeleteReadingListsDto.cs rename to Kavita.Models/DTOs/ReadingLists/DeleteReadingListsDto.cs index 8ce92f939..8feb44955 100644 --- a/API/DTOs/ReadingLists/DeleteReadingListsDto.cs +++ b/Kavita.Models/DTOs/ReadingLists/DeleteReadingListsDto.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; public sealed record DeleteReadingListsDto { diff --git a/API/DTOs/ReadingLists/PromoteReadingListsDto.cs b/Kavita.Models/DTOs/ReadingLists/PromoteReadingListsDto.cs similarity index 80% rename from API/DTOs/ReadingLists/PromoteReadingListsDto.cs rename to Kavita.Models/DTOs/ReadingLists/PromoteReadingListsDto.cs index 8915274de..6f3a6c729 100644 --- a/API/DTOs/ReadingLists/PromoteReadingListsDto.cs +++ b/Kavita.Models/DTOs/ReadingLists/PromoteReadingListsDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; public sealed record PromoteReadingListsDto { diff --git a/API/DTOs/ReadingLists/ReadingListCast.cs b/Kavita.Models/DTOs/ReadingLists/ReadingListCast.cs similarity index 92% rename from API/DTOs/ReadingLists/ReadingListCast.cs rename to Kavita.Models/DTOs/ReadingLists/ReadingListCast.cs index 855bb12b7..dc8533e78 100644 --- a/API/DTOs/ReadingLists/ReadingListCast.cs +++ b/Kavita.Models/DTOs/ReadingLists/ReadingListCast.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using API.DTOs.Person; +using Kavita.Models.DTOs.Person; -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; public sealed record ReadingListCast { diff --git a/API/DTOs/ReadingLists/ReadingListDto.cs b/Kavita.Models/DTOs/ReadingLists/ReadingListDto.cs similarity index 93% rename from API/DTOs/ReadingLists/ReadingListDto.cs rename to Kavita.Models/DTOs/ReadingLists/ReadingListDto.cs index b1296f87a..d5a274399 100644 --- a/API/DTOs/ReadingLists/ReadingListDto.cs +++ b/Kavita.Models/DTOs/ReadingLists/ReadingListDto.cs @@ -1,7 +1,7 @@ -using API.Entities.Enums; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; #nullable enable public sealed record ReadingListDto : IHasCoverImage diff --git a/API/DTOs/ReadingLists/ReadingListInfoDto.cs b/Kavita.Models/DTOs/ReadingLists/ReadingListInfoDto.cs similarity index 89% rename from API/DTOs/ReadingLists/ReadingListInfoDto.cs rename to Kavita.Models/DTOs/ReadingLists/ReadingListInfoDto.cs index b1655f850..ef3875e12 100644 --- a/API/DTOs/ReadingLists/ReadingListInfoDto.cs +++ b/Kavita.Models/DTOs/ReadingLists/ReadingListInfoDto.cs @@ -1,6 +1,6 @@ -using API.Entities.Interfaces; +using Kavita.Models.Entities.Interfaces; -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; public sealed record ReadingListInfoDto : IHasReadTimeEstimate { diff --git a/API/DTOs/ReadingLists/ReadingListItemDto.cs b/Kavita.Models/DTOs/ReadingLists/ReadingListItemDto.cs similarity index 95% rename from API/DTOs/ReadingLists/ReadingListItemDto.cs rename to Kavita.Models/DTOs/ReadingLists/ReadingListItemDto.cs index de3216a51..ed347579f 100644 --- a/API/DTOs/ReadingLists/ReadingListItemDto.cs +++ b/Kavita.Models/DTOs/ReadingLists/ReadingListItemDto.cs @@ -1,7 +1,7 @@ using System; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; #nullable enable public sealed record ReadingListItemDto diff --git a/API/DTOs/ReadingLists/UpdateReadingListByChapterDto.cs b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListByChapterDto.cs similarity index 79% rename from API/DTOs/ReadingLists/UpdateReadingListByChapterDto.cs rename to Kavita.Models/DTOs/ReadingLists/UpdateReadingListByChapterDto.cs index 6624c8a5c..a762c3d33 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListByChapterDto.cs +++ b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListByChapterDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; public sealed record UpdateReadingListByChapterDto { diff --git a/API/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs similarity index 87% rename from API/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs rename to Kavita.Models/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs index ba7625088..5145026fe 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs +++ b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; public sealed record UpdateReadingListByMultipleDto { diff --git a/API/DTOs/ReadingLists/UpdateReadingListByMultipleSeriesDto.cs b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListByMultipleSeriesDto.cs similarity index 83% rename from API/DTOs/ReadingLists/UpdateReadingListByMultipleSeriesDto.cs rename to Kavita.Models/DTOs/ReadingLists/UpdateReadingListByMultipleSeriesDto.cs index 910a5744d..4a3ec3007 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListByMultipleSeriesDto.cs +++ b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListByMultipleSeriesDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; public sealed record UpdateReadingListByMultipleSeriesDto { diff --git a/API/DTOs/ReadingLists/UpdateReadingListBySeriesDto.cs b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListBySeriesDto.cs similarity index 75% rename from API/DTOs/ReadingLists/UpdateReadingListBySeriesDto.cs rename to Kavita.Models/DTOs/ReadingLists/UpdateReadingListBySeriesDto.cs index 4bb4aa7bb..35a1969a3 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListBySeriesDto.cs +++ b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListBySeriesDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; public sealed record UpdateReadingListBySeriesDto { diff --git a/API/DTOs/ReadingLists/UpdateReadingListByVolumeDto.cs b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListByVolumeDto.cs similarity index 79% rename from API/DTOs/ReadingLists/UpdateReadingListByVolumeDto.cs rename to Kavita.Models/DTOs/ReadingLists/UpdateReadingListByVolumeDto.cs index 422d1cc34..d43fddf80 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListByVolumeDto.cs +++ b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListByVolumeDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; public sealed record UpdateReadingListByVolumeDto { diff --git a/API/DTOs/ReadingLists/UpdateReadingListDto.cs b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListDto.cs similarity index 92% rename from API/DTOs/ReadingLists/UpdateReadingListDto.cs rename to Kavita.Models/DTOs/ReadingLists/UpdateReadingListDto.cs index de273d825..fbc4f469c 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListDto.cs +++ b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListDto.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; public sealed record UpdateReadingListDto { diff --git a/API/DTOs/ReadingLists/UpdateReadingListPosition.cs b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListPosition.cs similarity index 90% rename from API/DTOs/ReadingLists/UpdateReadingListPosition.cs rename to Kavita.Models/DTOs/ReadingLists/UpdateReadingListPosition.cs index 04f2501a8..e2fc14825 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListPosition.cs +++ b/Kavita.Models/DTOs/ReadingLists/UpdateReadingListPosition.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace API.DTOs.ReadingLists; +namespace Kavita.Models.DTOs.ReadingLists; /// /// DTO for moving a reading list item to another position within the same list diff --git a/API/DTOs/Recommendation/ExternalSeriesDto.cs b/Kavita.Models/DTOs/Recommendation/ExternalSeriesDto.cs similarity index 82% rename from API/DTOs/Recommendation/ExternalSeriesDto.cs rename to Kavita.Models/DTOs/Recommendation/ExternalSeriesDto.cs index 752001a39..30f158a5c 100644 --- a/API/DTOs/Recommendation/ExternalSeriesDto.cs +++ b/Kavita.Models/DTOs/Recommendation/ExternalSeriesDto.cs @@ -1,6 +1,8 @@ -using API.Services.Plus; + -namespace API.DTOs.Recommendation; +using Kavita.Models.Entities.Enums; + +namespace Kavita.Models.DTOs.Recommendation; #nullable enable public sealed record ExternalSeriesDto diff --git a/API/DTOs/Recommendation/MetadataTagDto.cs b/Kavita.Models/DTOs/Recommendation/MetadataTagDto.cs similarity index 87% rename from API/DTOs/Recommendation/MetadataTagDto.cs rename to Kavita.Models/DTOs/Recommendation/MetadataTagDto.cs index a7eb76284..203854209 100644 --- a/API/DTOs/Recommendation/MetadataTagDto.cs +++ b/Kavita.Models/DTOs/Recommendation/MetadataTagDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Recommendation; +namespace Kavita.Models.DTOs.Recommendation; public sealed record MetadataTagDto { diff --git a/API/DTOs/Recommendation/RecommendationDto.cs b/Kavita.Models/DTOs/Recommendation/RecommendationDto.cs similarity index 85% rename from API/DTOs/Recommendation/RecommendationDto.cs rename to Kavita.Models/DTOs/Recommendation/RecommendationDto.cs index 387661324..e11fa894f 100644 --- a/API/DTOs/Recommendation/RecommendationDto.cs +++ b/Kavita.Models/DTOs/Recommendation/RecommendationDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Recommendation; +namespace Kavita.Models.DTOs.Recommendation; public sealed record RecommendationDto { diff --git a/API/DTOs/Recommendation/SeriesStaffDto.cs b/Kavita.Models/DTOs/Recommendation/SeriesStaffDto.cs similarity index 89% rename from API/DTOs/Recommendation/SeriesStaffDto.cs rename to Kavita.Models/DTOs/Recommendation/SeriesStaffDto.cs index e074e8625..6b3bbd032 100644 --- a/API/DTOs/Recommendation/SeriesStaffDto.cs +++ b/Kavita.Models/DTOs/Recommendation/SeriesStaffDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Recommendation; +namespace Kavita.Models.DTOs.Recommendation; #nullable enable public sealed record SeriesStaffDto diff --git a/API/DTOs/RefreshSeriesDto.cs b/Kavita.Models/DTOs/RefreshSeriesDto.cs similarity index 95% rename from API/DTOs/RefreshSeriesDto.cs rename to Kavita.Models/DTOs/RefreshSeriesDto.cs index ad26afba2..cf4afd260 100644 --- a/API/DTOs/RefreshSeriesDto.cs +++ b/Kavita.Models/DTOs/RefreshSeriesDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs; +namespace Kavita.Models.DTOs; /// /// Used for running some task against a Series. diff --git a/API/DTOs/RegisterDto.cs b/Kavita.Models/DTOs/RegisterDto.cs similarity index 93% rename from API/DTOs/RegisterDto.cs rename to Kavita.Models/DTOs/RegisterDto.cs index e117af872..64d626c54 100644 --- a/API/DTOs/RegisterDto.cs +++ b/Kavita.Models/DTOs/RegisterDto.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable public sealed record RegisterDto diff --git a/API/DTOs/ScanFolderDto.cs b/Kavita.Models/DTOs/ScanFolderDto.cs similarity index 95% rename from API/DTOs/ScanFolderDto.cs rename to Kavita.Models/DTOs/ScanFolderDto.cs index bfa669eec..7416d6ccb 100644 --- a/API/DTOs/ScanFolderDto.cs +++ b/Kavita.Models/DTOs/ScanFolderDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs; +namespace Kavita.Models.DTOs; /// /// DTO for requesting a folder to be scanned diff --git a/API/DTOs/Scrobbling/MalUserInfoDto.cs b/Kavita.Models/DTOs/Scrobbling/MalUserInfoDto.cs similarity index 87% rename from API/DTOs/Scrobbling/MalUserInfoDto.cs rename to Kavita.Models/DTOs/Scrobbling/MalUserInfoDto.cs index b6fefc053..1e32d1b13 100644 --- a/API/DTOs/Scrobbling/MalUserInfoDto.cs +++ b/Kavita.Models/DTOs/Scrobbling/MalUserInfoDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Scrobbling; +namespace Kavita.Models.DTOs.Scrobbling; /// /// Information about a User's MAL connection diff --git a/API/DTOs/Scrobbling/MediaRecommendationDto.cs b/Kavita.Models/DTOs/Scrobbling/MediaRecommendationDto.cs similarity index 86% rename from API/DTOs/Scrobbling/MediaRecommendationDto.cs rename to Kavita.Models/DTOs/Scrobbling/MediaRecommendationDto.cs index 476d77279..8c4041d09 100644 --- a/API/DTOs/Scrobbling/MediaRecommendationDto.cs +++ b/Kavita.Models/DTOs/Scrobbling/MediaRecommendationDto.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using API.Services.Plus; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Scrobbling; +namespace Kavita.Models.DTOs.Scrobbling; #nullable enable public sealed record MediaRecommendationDto diff --git a/API/DTOs/Scrobbling/PlusSeriesDto.cs b/Kavita.Models/DTOs/Scrobbling/PlusSeriesDto.cs similarity index 95% rename from API/DTOs/Scrobbling/PlusSeriesDto.cs rename to Kavita.Models/DTOs/Scrobbling/PlusSeriesDto.cs index e089ded72..c6be75cf1 100644 --- a/API/DTOs/Scrobbling/PlusSeriesDto.cs +++ b/Kavita.Models/DTOs/Scrobbling/PlusSeriesDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Scrobbling; +namespace Kavita.Models.DTOs.Scrobbling; #nullable enable /// diff --git a/API/DTOs/Scrobbling/ScrobbleDto.cs b/Kavita.Models/DTOs/Scrobbling/ScrobbleDto.cs similarity index 98% rename from API/DTOs/Scrobbling/ScrobbleDto.cs rename to Kavita.Models/DTOs/Scrobbling/ScrobbleDto.cs index 7c440b61c..d937e34ee 100644 --- a/API/DTOs/Scrobbling/ScrobbleDto.cs +++ b/Kavita.Models/DTOs/Scrobbling/ScrobbleDto.cs @@ -1,7 +1,7 @@ using System; using System.ComponentModel; -namespace API.DTOs.Scrobbling; +namespace Kavita.Models.DTOs.Scrobbling; #nullable enable public enum ScrobbleEventType diff --git a/API/DTOs/Scrobbling/ScrobbleErrorDto.cs b/Kavita.Models/DTOs/Scrobbling/ScrobbleErrorDto.cs similarity index 90% rename from API/DTOs/Scrobbling/ScrobbleErrorDto.cs rename to Kavita.Models/DTOs/Scrobbling/ScrobbleErrorDto.cs index 7caaad1ca..31ba071f0 100644 --- a/API/DTOs/Scrobbling/ScrobbleErrorDto.cs +++ b/Kavita.Models/DTOs/Scrobbling/ScrobbleErrorDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.Scrobbling; +namespace Kavita.Models.DTOs.Scrobbling; public sealed record ScrobbleErrorDto { diff --git a/API/DTOs/Scrobbling/ScrobbleEventDto.cs b/Kavita.Models/DTOs/Scrobbling/ScrobbleEventDto.cs similarity index 94% rename from API/DTOs/Scrobbling/ScrobbleEventDto.cs rename to Kavita.Models/DTOs/Scrobbling/ScrobbleEventDto.cs index 562d923ff..42cfcc39c 100644 --- a/API/DTOs/Scrobbling/ScrobbleEventDto.cs +++ b/Kavita.Models/DTOs/Scrobbling/ScrobbleEventDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.Scrobbling; +namespace Kavita.Models.DTOs.Scrobbling; #nullable enable public sealed record ScrobbleEventDto diff --git a/API/DTOs/Scrobbling/ScrobbleHoldDto.cs b/Kavita.Models/DTOs/Scrobbling/ScrobbleHoldDto.cs similarity index 86% rename from API/DTOs/Scrobbling/ScrobbleHoldDto.cs rename to Kavita.Models/DTOs/Scrobbling/ScrobbleHoldDto.cs index 3e09e4799..77cd1b151 100644 --- a/API/DTOs/Scrobbling/ScrobbleHoldDto.cs +++ b/Kavita.Models/DTOs/Scrobbling/ScrobbleHoldDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.Scrobbling; +namespace Kavita.Models.DTOs.Scrobbling; public sealed record ScrobbleHoldDto { diff --git a/API/DTOs/Scrobbling/ScrobbleResponseDto.cs b/Kavita.Models/DTOs/Scrobbling/ScrobbleResponseDto.cs similarity index 87% rename from API/DTOs/Scrobbling/ScrobbleResponseDto.cs rename to Kavita.Models/DTOs/Scrobbling/ScrobbleResponseDto.cs index ad66729d0..8920b71e7 100644 --- a/API/DTOs/Scrobbling/ScrobbleResponseDto.cs +++ b/Kavita.Models/DTOs/Scrobbling/ScrobbleResponseDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Scrobbling; +namespace Kavita.Models.DTOs.Scrobbling; #nullable enable /// diff --git a/API/DTOs/Search/BookmarkSearchResultDto.cs b/Kavita.Models/DTOs/Search/BookmarkSearchResultDto.cs similarity index 88% rename from API/DTOs/Search/BookmarkSearchResultDto.cs rename to Kavita.Models/DTOs/Search/BookmarkSearchResultDto.cs index c11d2a2b8..4c94547b3 100644 --- a/API/DTOs/Search/BookmarkSearchResultDto.cs +++ b/Kavita.Models/DTOs/Search/BookmarkSearchResultDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Search; +namespace Kavita.Models.DTOs.Search; public sealed record BookmarkSearchResultDto { diff --git a/API/DTOs/Search/SearchResultDto.cs b/Kavita.Models/DTOs/Search/SearchResultDto.cs similarity index 86% rename from API/DTOs/Search/SearchResultDto.cs rename to Kavita.Models/DTOs/Search/SearchResultDto.cs index c497b55dd..40837fa33 100644 --- a/API/DTOs/Search/SearchResultDto.cs +++ b/Kavita.Models/DTOs/Search/SearchResultDto.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Search; +namespace Kavita.Models.DTOs.Search; public sealed record SearchResultDto { diff --git a/API/DTOs/Search/SearchResultGroupDto.cs b/Kavita.Models/DTOs/Search/SearchResultGroupDto.cs similarity index 81% rename from API/DTOs/Search/SearchResultGroupDto.cs rename to Kavita.Models/DTOs/Search/SearchResultGroupDto.cs index e3edcaf29..cd0635ee7 100644 --- a/API/DTOs/Search/SearchResultGroupDto.cs +++ b/Kavita.Models/DTOs/Search/SearchResultGroupDto.cs @@ -1,11 +1,11 @@ using System.Collections.Generic; -using API.DTOs.Collection; -using API.DTOs.Metadata; -using API.DTOs.Person; -using API.DTOs.Reader; -using API.DTOs.ReadingLists; +using Kavita.Models.DTOs.Collection; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.DTOs.ReadingLists; -namespace API.DTOs.Search; +namespace Kavita.Models.DTOs.Search; /// /// Represents all Search results for a query diff --git a/API/DTOs/SeriesByIdsDto.cs b/Kavita.Models/DTOs/SeriesByIdsDto.cs similarity index 74% rename from API/DTOs/SeriesByIdsDto.cs rename to Kavita.Models/DTOs/SeriesByIdsDto.cs index cb4c52b1e..971bbc517 100644 --- a/API/DTOs/SeriesByIdsDto.cs +++ b/Kavita.Models/DTOs/SeriesByIdsDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record SeriesByIdsDto { diff --git a/API/DTOs/SeriesDetail/NextExpectedChapterDto.cs b/Kavita.Models/DTOs/SeriesDetail/NextExpectedChapterDto.cs similarity index 90% rename from API/DTOs/SeriesDetail/NextExpectedChapterDto.cs rename to Kavita.Models/DTOs/SeriesDetail/NextExpectedChapterDto.cs index 1bea81c84..41492b100 100644 --- a/API/DTOs/SeriesDetail/NextExpectedChapterDto.cs +++ b/Kavita.Models/DTOs/SeriesDetail/NextExpectedChapterDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.SeriesDetail; +namespace Kavita.Models.DTOs.SeriesDetail; public sealed record NextExpectedChapterDto { diff --git a/API/Data/Misc/RecentlyAddedSeries.cs b/Kavita.Models/DTOs/SeriesDetail/RecentlyAddedSeriesDto.cs similarity index 83% rename from API/Data/Misc/RecentlyAddedSeries.cs rename to Kavita.Models/DTOs/SeriesDetail/RecentlyAddedSeriesDto.cs index 1ea5b1d3e..c00897dbf 100644 --- a/API/Data/Misc/RecentlyAddedSeries.cs +++ b/Kavita.Models/DTOs/SeriesDetail/RecentlyAddedSeriesDto.cs @@ -1,10 +1,10 @@ -using System; -using API.Entities.Enums; - -namespace API.Data.Misc; #nullable enable +using System; +using Kavita.Models.Entities.Enums; -public class RecentlyAddedSeries +namespace Kavita.Models.DTOs.SeriesDetail; + +public class RecentlyAddedSeriesDto { public int LibraryId { get; init; } public LibraryType LibraryType { get; init; } diff --git a/API/DTOs/SeriesDetail/RelatedSeriesDto.cs b/Kavita.Models/DTOs/SeriesDetail/RelatedSeriesDto.cs similarity index 96% rename from API/DTOs/SeriesDetail/RelatedSeriesDto.cs rename to Kavita.Models/DTOs/SeriesDetail/RelatedSeriesDto.cs index a186dc295..7f34ca071 100644 --- a/API/DTOs/SeriesDetail/RelatedSeriesDto.cs +++ b/Kavita.Models/DTOs/SeriesDetail/RelatedSeriesDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.SeriesDetail; +namespace Kavita.Models.DTOs.SeriesDetail; public sealed record RelatedSeriesDto { diff --git a/API/DTOs/SeriesDetail/SeriesDetailDto.cs b/Kavita.Models/DTOs/SeriesDetail/SeriesDetailDto.cs similarity index 96% rename from API/DTOs/SeriesDetail/SeriesDetailDto.cs rename to Kavita.Models/DTOs/SeriesDetail/SeriesDetailDto.cs index c4f15552d..43474a5f8 100644 --- a/API/DTOs/SeriesDetail/SeriesDetailDto.cs +++ b/Kavita.Models/DTOs/SeriesDetail/SeriesDetailDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.SeriesDetail; +namespace Kavita.Models.DTOs.SeriesDetail; #nullable enable /// diff --git a/API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs b/Kavita.Models/DTOs/SeriesDetail/SeriesDetailPlusDto.cs similarity index 78% rename from API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs rename to Kavita.Models/DTOs/SeriesDetail/SeriesDetailPlusDto.cs index 95f5f39bd..2e3952b2d 100644 --- a/API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs +++ b/Kavita.Models/DTOs/SeriesDetail/SeriesDetailPlusDto.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Recommendation; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Recommendation; -namespace API.DTOs.SeriesDetail; +namespace Kavita.Models.DTOs.SeriesDetail; #nullable enable /// diff --git a/API/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs b/Kavita.Models/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs similarity index 95% rename from API/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs rename to Kavita.Models/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs index a1bb2057e..1cd28ea10 100644 --- a/API/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs +++ b/Kavita.Models/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.SeriesDetail; +namespace Kavita.Models.DTOs.SeriesDetail; public sealed record UpdateRelatedSeriesDto { diff --git a/API/DTOs/SeriesDetail/UpdateUserReviewDto.cs b/Kavita.Models/DTOs/SeriesDetail/UpdateUserReviewDto.cs similarity index 80% rename from API/DTOs/SeriesDetail/UpdateUserReviewDto.cs rename to Kavita.Models/DTOs/SeriesDetail/UpdateUserReviewDto.cs index 7af9441c1..fa679965d 100644 --- a/API/DTOs/SeriesDetail/UpdateUserReviewDto.cs +++ b/Kavita.Models/DTOs/SeriesDetail/UpdateUserReviewDto.cs @@ -1,5 +1,5 @@  -namespace API.DTOs.SeriesDetail; +namespace Kavita.Models.DTOs.SeriesDetail; #nullable enable public sealed record UpdateUserReviewDto diff --git a/API/DTOs/SeriesDetail/UserReviewDto.cs b/Kavita.Models/DTOs/SeriesDetail/UserReviewDto.cs similarity index 95% rename from API/DTOs/SeriesDetail/UserReviewDto.cs rename to Kavita.Models/DTOs/SeriesDetail/UserReviewDto.cs index 8d695d7e6..982868da5 100644 --- a/API/DTOs/SeriesDetail/UserReviewDto.cs +++ b/Kavita.Models/DTOs/SeriesDetail/UserReviewDto.cs @@ -1,7 +1,6 @@ -using API.Entities.Enums; -using API.Services.Plus; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.SeriesDetail; +namespace Kavita.Models.DTOs.SeriesDetail; #nullable enable /// diff --git a/API/DTOs/SeriesDetail/UserReviewExtendedDto.cs b/Kavita.Models/DTOs/SeriesDetail/UserReviewExtendedDto.cs similarity index 91% rename from API/DTOs/SeriesDetail/UserReviewExtendedDto.cs rename to Kavita.Models/DTOs/SeriesDetail/UserReviewExtendedDto.cs index 5a656e25e..6ecbcd673 100644 --- a/API/DTOs/SeriesDetail/UserReviewExtendedDto.cs +++ b/Kavita.Models/DTOs/SeriesDetail/UserReviewExtendedDto.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using API.DTOs.Person; +using Kavita.Models.DTOs.Person; -namespace API.DTOs.SeriesDetail; +namespace Kavita.Models.DTOs.SeriesDetail; #nullable enable diff --git a/API/DTOs/SeriesDto.cs b/Kavita.Models/DTOs/SeriesDto.cs similarity index 97% rename from API/DTOs/SeriesDto.cs rename to Kavita.Models/DTOs/SeriesDto.cs index 03f0d04b3..4a3edd417 100644 --- a/API/DTOs/SeriesDto.cs +++ b/Kavita.Models/DTOs/SeriesDto.cs @@ -1,8 +1,8 @@ using System; -using API.Entities.Enums; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable public sealed record SeriesDto : IHasReadTimeEstimate, IHasCoverImage diff --git a/API/DTOs/SeriesMetadataDto.cs b/Kavita.Models/DTOs/SeriesMetadataDto.cs similarity index 96% rename from API/DTOs/SeriesMetadataDto.cs rename to Kavita.Models/DTOs/SeriesMetadataDto.cs index 562335da1..75f646b36 100644 --- a/API/DTOs/SeriesMetadataDto.cs +++ b/Kavita.Models/DTOs/SeriesMetadataDto.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; -using API.DTOs.Metadata; -using API.DTOs.Person; -using API.Entities.Enums; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Person; +using Kavita.Models.Entities.Enums; -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable public sealed record SeriesMetadataDto diff --git a/API/DTOs/Settings/AuthorityValidationDto.cs b/Kavita.Models/DTOs/Settings/AuthorityValidationDto.cs similarity index 60% rename from API/DTOs/Settings/AuthorityValidationDto.cs rename to Kavita.Models/DTOs/Settings/AuthorityValidationDto.cs index e7ea2ae18..3414eea7b 100644 --- a/API/DTOs/Settings/AuthorityValidationDto.cs +++ b/Kavita.Models/DTOs/Settings/AuthorityValidationDto.cs @@ -1,3 +1,3 @@ -namespace API.DTOs.Settings; +namespace Kavita.Models.DTOs.Settings; public sealed record AuthorityValidationDto(string Authority); diff --git a/API/DTOs/Settings/ImportFieldMappingsDto.cs b/Kavita.Models/DTOs/Settings/ImportFieldMappingsDto.cs similarity index 75% rename from API/DTOs/Settings/ImportFieldMappingsDto.cs rename to Kavita.Models/DTOs/Settings/ImportFieldMappingsDto.cs index 2699292b2..b7bb4f8e9 100644 --- a/API/DTOs/Settings/ImportFieldMappingsDto.cs +++ b/Kavita.Models/DTOs/Settings/ImportFieldMappingsDto.cs @@ -1,6 +1,6 @@ -using API.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.KavitaPlus.Metadata; -namespace API.DTOs.Settings; +namespace Kavita.Models.DTOs.Settings; public sealed record ImportFieldMappingsDto { diff --git a/API/DTOs/Settings/OidcConfigDto.cs b/Kavita.Models/DTOs/Settings/OidcConfigDto.cs similarity index 97% rename from API/DTOs/Settings/OidcConfigDto.cs rename to Kavita.Models/DTOs/Settings/OidcConfigDto.cs index db065b5f0..dc4be353e 100644 --- a/API/DTOs/Settings/OidcConfigDto.cs +++ b/Kavita.Models/DTOs/Settings/OidcConfigDto.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Security.Claims; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Settings; +namespace Kavita.Models.DTOs.Settings; /// /// All configuration regarding OIDC diff --git a/API/DTOs/Settings/OidcPublicConfigDto.cs b/Kavita.Models/DTOs/Settings/OidcPublicConfigDto.cs similarity index 94% rename from API/DTOs/Settings/OidcPublicConfigDto.cs rename to Kavita.Models/DTOs/Settings/OidcPublicConfigDto.cs index 6843adcca..4d94d60a0 100644 --- a/API/DTOs/Settings/OidcPublicConfigDto.cs +++ b/Kavita.Models/DTOs/Settings/OidcPublicConfigDto.cs @@ -1,6 +1,6 @@ #nullable enable -namespace API.DTOs.Settings; +namespace Kavita.Models.DTOs.Settings; /** * The part of the OIDC configuration that is returned by the API without authentication diff --git a/API/DTOs/Settings/SMTPConfigDto.cs b/Kavita.Models/DTOs/Settings/SMTPConfigDto.cs similarity index 94% rename from API/DTOs/Settings/SMTPConfigDto.cs rename to Kavita.Models/DTOs/Settings/SMTPConfigDto.cs index c14140062..065e91ba0 100644 --- a/API/DTOs/Settings/SMTPConfigDto.cs +++ b/Kavita.Models/DTOs/Settings/SMTPConfigDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Settings; +namespace Kavita.Models.DTOs.Settings; public sealed record SmtpConfigDto { diff --git a/API/DTOs/Settings/ServerSettingDTO.cs b/Kavita.Models/DTOs/Settings/ServerSettingDTO.cs similarity index 98% rename from API/DTOs/Settings/ServerSettingDTO.cs rename to Kavita.Models/DTOs/Settings/ServerSettingDTO.cs index dbc894412..b00b241f3 100644 --- a/API/DTOs/Settings/ServerSettingDTO.cs +++ b/Kavita.Models/DTOs/Settings/ServerSettingDTO.cs @@ -1,8 +1,7 @@ using System; -using API.Entities.Enums; -using API.Services; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Settings; +namespace Kavita.Models.DTOs.Settings; #nullable enable public sealed record ServerSettingDto diff --git a/API/DTOs/SideNav/BulkUpdateSideNavStreamVisibilityDto.cs b/Kavita.Models/DTOs/SideNav/BulkUpdateSideNavStreamVisibilityDto.cs similarity index 84% rename from API/DTOs/SideNav/BulkUpdateSideNavStreamVisibilityDto.cs rename to Kavita.Models/DTOs/SideNav/BulkUpdateSideNavStreamVisibilityDto.cs index ae1d927a9..a84c5bdb5 100644 --- a/API/DTOs/SideNav/BulkUpdateSideNavStreamVisibilityDto.cs +++ b/Kavita.Models/DTOs/SideNav/BulkUpdateSideNavStreamVisibilityDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.SideNav; +namespace Kavita.Models.DTOs.SideNav; public sealed record BulkUpdateSideNavStreamVisibilityDto { diff --git a/API/DTOs/SideNav/ExternalSourceDto.cs b/Kavita.Models/DTOs/SideNav/ExternalSourceDto.cs similarity index 84% rename from API/DTOs/SideNav/ExternalSourceDto.cs rename to Kavita.Models/DTOs/SideNav/ExternalSourceDto.cs index ef79f1d89..618fd0838 100644 --- a/API/DTOs/SideNav/ExternalSourceDto.cs +++ b/Kavita.Models/DTOs/SideNav/ExternalSourceDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.SideNav; +namespace Kavita.Models.DTOs.SideNav; public sealed record ExternalSourceDto { diff --git a/API/DTOs/SideNav/SideNavStreamDto.cs b/Kavita.Models/DTOs/SideNav/SideNavStreamDto.cs similarity index 93% rename from API/DTOs/SideNav/SideNavStreamDto.cs rename to Kavita.Models/DTOs/SideNav/SideNavStreamDto.cs index ec6cdf5a7..11ccd7fd0 100644 --- a/API/DTOs/SideNav/SideNavStreamDto.cs +++ b/Kavita.Models/DTOs/SideNav/SideNavStreamDto.cs @@ -1,6 +1,6 @@ -using API.Entities; +using Kavita.Models.Entities; -namespace API.DTOs.SideNav; +namespace Kavita.Models.DTOs.SideNav; #nullable enable public sealed record SideNavStreamDto diff --git a/API/SignalR/MessageFactory.cs b/Kavita.Models/DTOs/SignalR/MessageFactory.cs similarity index 97% rename from API/SignalR/MessageFactory.cs rename to Kavita.Models/DTOs/SignalR/MessageFactory.cs index bb0e7c776..7597a3346 100644 --- a/API/SignalR/MessageFactory.cs +++ b/Kavita.Models/DTOs/SignalR/MessageFactory.cs @@ -1,12 +1,11 @@ using System; -using API.DTOs.Account; -using API.DTOs.Reader; -using API.DTOs.Update; -using API.Entities.Person; -using API.Extensions; -using API.Services.Plus; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs.Account; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.DTOs.Update; +using Kavita.Models.Entities.Enums; -namespace API.SignalR; +namespace Kavita.Models.DTOs.SignalR; public static class MessageFactoryEntityTypes { @@ -148,6 +147,10 @@ public static class MessageFactory /// public const string ChapterRemoved = "ChapterRemoved"; /// + /// Chapter is updated + /// + public const string ChapterUpdated = "ChapterUpdated"; + /// /// Volume is removed from server /// public const string VolumeRemoved = "VolumeRemoved"; @@ -269,6 +272,19 @@ public static class MessageFactory }; } + public static SignalRMessage ChapterUpdatedEvent(int chapterId, int seriesId) + { + return new SignalRMessage + { + Name = ChapterUpdated, + Body = new + { + SeriesId = seriesId, + ChapterId = chapterId + } + }; + } + public static SignalRMessage VolumeRemovedEvent(int volumeId, int seriesId) { return new SignalRMessage() @@ -703,7 +719,7 @@ public static class MessageFactory }; } - public static SignalRMessage PersonMergedMessage(Person dst, Person src) + public static SignalRMessage PersonMergedMessage(Entities.Person.Person dst, Entities.Person.Person src) { return new SignalRMessage() { diff --git a/API/SignalR/ProgressEventType.cs b/Kavita.Models/DTOs/SignalR/ProgressEventType.cs similarity index 89% rename from API/SignalR/ProgressEventType.cs rename to Kavita.Models/DTOs/SignalR/ProgressEventType.cs index 89ba758c5..34cc9ebc4 100644 --- a/API/SignalR/ProgressEventType.cs +++ b/Kavita.Models/DTOs/SignalR/ProgressEventType.cs @@ -1,4 +1,4 @@ -namespace API.SignalR; +namespace Kavita.Models.DTOs.SignalR; public static class ProgressEventType { diff --git a/API/SignalR/ProgressType.cs b/Kavita.Models/DTOs/SignalR/ProgressType.cs similarity index 92% rename from API/SignalR/ProgressType.cs rename to Kavita.Models/DTOs/SignalR/ProgressType.cs index b0fbe341d..9cac78616 100644 --- a/API/SignalR/ProgressType.cs +++ b/Kavita.Models/DTOs/SignalR/ProgressType.cs @@ -1,4 +1,4 @@ -namespace API.SignalR; +namespace Kavita.Models.DTOs.SignalR; /// /// How progress should be represented on the UI diff --git a/API/SignalR/SignalRMessage.cs b/Kavita.Models/DTOs/SignalR/SignalRMessage.cs similarity index 96% rename from API/SignalR/SignalRMessage.cs rename to Kavita.Models/DTOs/SignalR/SignalRMessage.cs index f00a677b9..0dfc11b17 100644 --- a/API/SignalR/SignalRMessage.cs +++ b/Kavita.Models/DTOs/SignalR/SignalRMessage.cs @@ -1,6 +1,6 @@ using System; -namespace API.SignalR; +namespace Kavita.Models.DTOs.SignalR; #nullable enable /// diff --git a/API/DTOs/StandaloneChapterDto.cs b/Kavita.Models/DTOs/StandaloneChapterDto.cs similarity index 81% rename from API/DTOs/StandaloneChapterDto.cs rename to Kavita.Models/DTOs/StandaloneChapterDto.cs index 2f4cd2ee1..8d49c0c8a 100644 --- a/API/DTOs/StandaloneChapterDto.cs +++ b/Kavita.Models/DTOs/StandaloneChapterDto.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable /// diff --git a/API/DTOs/Statistics/BreakDownDto.cs b/Kavita.Models/DTOs/Statistics/BreakDownDto.cs similarity index 85% rename from API/DTOs/Statistics/BreakDownDto.cs rename to Kavita.Models/DTOs/Statistics/BreakDownDto.cs index f364dd4b3..3d5889f5f 100644 --- a/API/DTOs/Statistics/BreakDownDto.cs +++ b/Kavita.Models/DTOs/Statistics/BreakDownDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; public sealed record BreakDownDto { diff --git a/API/DTOs/Statistics/Count.cs b/Kavita.Models/DTOs/Statistics/Count.cs similarity index 75% rename from API/DTOs/Statistics/Count.cs rename to Kavita.Models/DTOs/Statistics/Count.cs index 1577e682c..de2bf5232 100644 --- a/API/DTOs/Statistics/Count.cs +++ b/Kavita.Models/DTOs/Statistics/Count.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; public sealed record StatCount : ICount { diff --git a/API/DTOs/Statistics/FileExtensionBreakdownDto.cs b/Kavita.Models/DTOs/Statistics/FileExtensionBreakdownDto.cs similarity index 86% rename from API/DTOs/Statistics/FileExtensionBreakdownDto.cs rename to Kavita.Models/DTOs/Statistics/FileExtensionBreakdownDto.cs index 7a248caef..84f3294be 100644 --- a/API/DTOs/Statistics/FileExtensionBreakdownDto.cs +++ b/Kavita.Models/DTOs/Statistics/FileExtensionBreakdownDto.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; #nullable enable public sealed record FileExtensionDto diff --git a/API/DTOs/Statistics/ICount.cs b/Kavita.Models/DTOs/Statistics/ICount.cs similarity index 69% rename from API/DTOs/Statistics/ICount.cs rename to Kavita.Models/DTOs/Statistics/ICount.cs index 7f8b5b2ed..9c66f801e 100644 --- a/API/DTOs/Statistics/ICount.cs +++ b/Kavita.Models/DTOs/Statistics/ICount.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; public interface ICount { diff --git a/API/DTOs/Statistics/MostActiveUserDto.cs b/Kavita.Models/DTOs/Statistics/MostActiveUserDto.cs similarity index 92% rename from API/DTOs/Statistics/MostActiveUserDto.cs rename to Kavita.Models/DTOs/Statistics/MostActiveUserDto.cs index 2f90ed429..043321e4a 100644 --- a/API/DTOs/Statistics/MostActiveUserDto.cs +++ b/Kavita.Models/DTOs/Statistics/MostActiveUserDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; #nullable enable diff --git a/API/DTOs/Statistics/MostReadAuthorsDto.cs b/Kavita.Models/DTOs/Statistics/MostReadAuthorsDto.cs similarity index 91% rename from API/DTOs/Statistics/MostReadAuthorsDto.cs rename to Kavita.Models/DTOs/Statistics/MostReadAuthorsDto.cs index 9fe2de530..94b742070 100644 --- a/API/DTOs/Statistics/MostReadAuthorsDto.cs +++ b/Kavita.Models/DTOs/Statistics/MostReadAuthorsDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; public sealed record MostReadAuthorsDto { diff --git a/API/DTOs/Statistics/PagesReadOnADayCount.cs b/Kavita.Models/DTOs/Statistics/PagesReadOnADayCount.cs similarity index 82% rename from API/DTOs/Statistics/PagesReadOnADayCount.cs rename to Kavita.Models/DTOs/Statistics/PagesReadOnADayCount.cs index 08a1f404c..fb8120040 100644 --- a/API/DTOs/Statistics/PagesReadOnADayCount.cs +++ b/Kavita.Models/DTOs/Statistics/PagesReadOnADayCount.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; public sealed record StatCountWithFormat : ICount { diff --git a/API/DTOs/Statistics/ProfileStatBarDto.cs b/Kavita.Models/DTOs/Statistics/ProfileStatBarDto.cs similarity index 88% rename from API/DTOs/Statistics/ProfileStatBarDto.cs rename to Kavita.Models/DTOs/Statistics/ProfileStatBarDto.cs index 5cba4021d..17d93ea61 100644 --- a/API/DTOs/Statistics/ProfileStatBarDto.cs +++ b/Kavita.Models/DTOs/Statistics/ProfileStatBarDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; public sealed record ProfileStatBarDto { diff --git a/API/DTOs/Statistics/ReadHistoryEvent.cs b/Kavita.Models/DTOs/Statistics/ReadHistoryEvent.cs similarity index 93% rename from API/DTOs/Statistics/ReadHistoryEvent.cs rename to Kavita.Models/DTOs/Statistics/ReadHistoryEvent.cs index 5d8262aef..53021f924 100644 --- a/API/DTOs/Statistics/ReadHistoryEvent.cs +++ b/Kavita.Models/DTOs/Statistics/ReadHistoryEvent.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; #nullable enable /// diff --git a/API/DTOs/Statistics/ReadTimeByHourDto.cs b/Kavita.Models/DTOs/Statistics/ReadTimeByHourDto.cs similarity index 82% rename from API/DTOs/Statistics/ReadTimeByHourDto.cs rename to Kavita.Models/DTOs/Statistics/ReadTimeByHourDto.cs index ee94302f6..f3226757e 100644 --- a/API/DTOs/Statistics/ReadTimeByHourDto.cs +++ b/Kavita.Models/DTOs/Statistics/ReadTimeByHourDto.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; public sealed record ReadTimeByHourDto { diff --git a/API/DTOs/Statistics/ReadingActivityGraphDto.cs b/Kavita.Models/DTOs/Statistics/ReadingActivityGraphDto.cs similarity index 91% rename from API/DTOs/Statistics/ReadingActivityGraphDto.cs rename to Kavita.Models/DTOs/Statistics/ReadingActivityGraphDto.cs index e11435c27..b1275a6c6 100644 --- a/API/DTOs/Statistics/ReadingActivityGraphDto.cs +++ b/Kavita.Models/DTOs/Statistics/ReadingActivityGraphDto.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; #nullable enable public sealed record ReadingActivityGraphEntryDto diff --git a/API/DTOs/Statistics/ReadingHistoryItemDto.cs b/Kavita.Models/DTOs/Statistics/ReadingHistoryItemDto.cs similarity index 94% rename from API/DTOs/Statistics/ReadingHistoryItemDto.cs rename to Kavita.Models/DTOs/Statistics/ReadingHistoryItemDto.cs index fdadc92a9..2cbac4d33 100644 --- a/API/DTOs/Statistics/ReadingHistoryItemDto.cs +++ b/Kavita.Models/DTOs/Statistics/ReadingHistoryItemDto.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; public sealed record ReadingHistoryItemDto { diff --git a/API/DTOs/Statistics/ReadingPaceDto.cs b/Kavita.Models/DTOs/Statistics/ReadingPaceDto.cs similarity index 86% rename from API/DTOs/Statistics/ReadingPaceDto.cs rename to Kavita.Models/DTOs/Statistics/ReadingPaceDto.cs index d8a1df50d..9bc7c65a8 100644 --- a/API/DTOs/Statistics/ReadingPaceDto.cs +++ b/Kavita.Models/DTOs/Statistics/ReadingPaceDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; public sealed record ReadingPaceDto { diff --git a/API/DTOs/Statistics/ServerStatisticsDto.cs b/Kavita.Models/DTOs/Statistics/ServerStatisticsDto.cs similarity index 90% rename from API/DTOs/Statistics/ServerStatisticsDto.cs rename to Kavita.Models/DTOs/Statistics/ServerStatisticsDto.cs index c67a30069..15b11e6ef 100644 --- a/API/DTOs/Statistics/ServerStatisticsDto.cs +++ b/Kavita.Models/DTOs/Statistics/ServerStatisticsDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; #nullable enable public sealed record ServerStatisticsDto diff --git a/API/DTOs/Statistics/SpreadStatsDto.cs b/Kavita.Models/DTOs/Statistics/SpreadStatsDto.cs similarity index 80% rename from API/DTOs/Statistics/SpreadStatsDto.cs rename to Kavita.Models/DTOs/Statistics/SpreadStatsDto.cs index 88a705ba6..c62fa2760 100644 --- a/API/DTOs/Statistics/SpreadStatsDto.cs +++ b/Kavita.Models/DTOs/Statistics/SpreadStatsDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; public sealed record SpreadStatsDto { diff --git a/API/DTOs/Statistics/StatBucketDto.cs b/Kavita.Models/DTOs/Statistics/StatBucketDto.cs similarity index 90% rename from API/DTOs/Statistics/StatBucketDto.cs rename to Kavita.Models/DTOs/Statistics/StatBucketDto.cs index ebd791bcd..bee8cd550 100644 --- a/API/DTOs/Statistics/StatBucketDto.cs +++ b/Kavita.Models/DTOs/Statistics/StatBucketDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; /// /// A bucket of items (fixed) from 0-X, X-X*2 diff --git a/API/DTOs/Statistics/StatsFilterDto.cs b/Kavita.Models/DTOs/Statistics/StatsFilterDto.cs similarity index 93% rename from API/DTOs/Statistics/StatsFilterDto.cs rename to Kavita.Models/DTOs/Statistics/StatsFilterDto.cs index a0c76524b..1c57b792b 100644 --- a/API/DTOs/Statistics/StatsFilterDto.cs +++ b/Kavita.Models/DTOs/Statistics/StatsFilterDto.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; #nullable enable public sealed record StatsFilterDto diff --git a/API/DTOs/Statistics/TopReadsDto.cs b/Kavita.Models/DTOs/Statistics/TopReadsDto.cs similarity index 90% rename from API/DTOs/Statistics/TopReadsDto.cs rename to Kavita.Models/DTOs/Statistics/TopReadsDto.cs index d11594dca..b5bb19146 100644 --- a/API/DTOs/Statistics/TopReadsDto.cs +++ b/Kavita.Models/DTOs/Statistics/TopReadsDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; #nullable enable public sealed record TopReadDto diff --git a/API/DTOs/Statistics/UserReadStatistics.cs b/Kavita.Models/DTOs/Statistics/UserReadStatistics.cs similarity index 94% rename from API/DTOs/Statistics/UserReadStatistics.cs rename to Kavita.Models/DTOs/Statistics/UserReadStatistics.cs index 2ff96fb61..f299fb618 100644 --- a/API/DTOs/Statistics/UserReadStatistics.cs +++ b/Kavita.Models/DTOs/Statistics/UserReadStatistics.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; #nullable enable public sealed record UserReadStatistics diff --git a/API/DTOs/Statistics/YearMonthGroupingDto.cs b/Kavita.Models/DTOs/Statistics/YearMonthGroupingDto.cs similarity index 73% rename from API/DTOs/Statistics/YearMonthGroupingDto.cs rename to Kavita.Models/DTOs/Statistics/YearMonthGroupingDto.cs index b080baac9..cf59e7a9d 100644 --- a/API/DTOs/Statistics/YearMonthGroupingDto.cs +++ b/Kavita.Models/DTOs/Statistics/YearMonthGroupingDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Statistics; +namespace Kavita.Models.DTOs.Statistics; public sealed record YearMonthGroupingDto { diff --git a/API/DTOs/Stats/FileExtensionExportDto.cs b/Kavita.Models/DTOs/Stats/FileExtensionExportDto.cs similarity index 89% rename from API/DTOs/Stats/FileExtensionExportDto.cs rename to Kavita.Models/DTOs/Stats/FileExtensionExportDto.cs index e881960a5..ff80d9e58 100644 --- a/API/DTOs/Stats/FileExtensionExportDto.cs +++ b/Kavita.Models/DTOs/Stats/FileExtensionExportDto.cs @@ -1,6 +1,6 @@ using CsvHelper.Configuration.Attributes; -namespace API.DTOs.Stats; +namespace Kavita.Models.DTOs.Stats; /// /// Excel export for File Extension Report diff --git a/API/DTOs/Stats/ServerInfoSlimDto.cs b/Kavita.Models/DTOs/Stats/ServerInfoSlimDto.cs similarity index 95% rename from API/DTOs/Stats/ServerInfoSlimDto.cs rename to Kavita.Models/DTOs/Stats/ServerInfoSlimDto.cs index f1abb2e1d..cdc07fbc2 100644 --- a/API/DTOs/Stats/ServerInfoSlimDto.cs +++ b/Kavita.Models/DTOs/Stats/ServerInfoSlimDto.cs @@ -1,6 +1,6 @@ using System; -namespace API.DTOs.Stats; +namespace Kavita.Models.DTOs.Stats; #nullable enable /// diff --git a/API/DTOs/Stats/V3/ClientDevice/DeviceClientBreakdownDto.cs b/Kavita.Models/DTOs/Stats/V3/ClientDevice/DeviceClientBreakdownDto.cs similarity index 61% rename from API/DTOs/Stats/V3/ClientDevice/DeviceClientBreakdownDto.cs rename to Kavita.Models/DTOs/Stats/V3/ClientDevice/DeviceClientBreakdownDto.cs index aca049e3c..f35ecbabf 100644 --- a/API/DTOs/Stats/V3/ClientDevice/DeviceClientBreakdownDto.cs +++ b/Kavita.Models/DTOs/Stats/V3/ClientDevice/DeviceClientBreakdownDto.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; -using API.DTOs.Statistics; -using API.Entities.Enums; +using Kavita.Models.DTOs.Statistics; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Stats.V3.ClientDevice; +namespace Kavita.Models.DTOs.Stats.V3.ClientDevice; public sealed record DeviceClientBreakdownDto { diff --git a/API/DTOs/Stats/V3/LibraryStatV3.cs b/Kavita.Models/DTOs/Stats/V3/LibraryStatV3.cs similarity index 94% rename from API/DTOs/Stats/V3/LibraryStatV3.cs rename to Kavita.Models/DTOs/Stats/V3/LibraryStatV3.cs index 461792666..e85877917 100644 --- a/API/DTOs/Stats/V3/LibraryStatV3.cs +++ b/Kavita.Models/DTOs/Stats/V3/LibraryStatV3.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Stats.V3; +namespace Kavita.Models.DTOs.Stats.V3; public sealed record LibraryStatV3 { diff --git a/API/DTOs/Stats/V3/RelationshipStatV3.cs b/Kavita.Models/DTOs/Stats/V3/RelationshipStatV3.cs similarity index 73% rename from API/DTOs/Stats/V3/RelationshipStatV3.cs rename to Kavita.Models/DTOs/Stats/V3/RelationshipStatV3.cs index 37b63cb9a..f7f22cdd6 100644 --- a/API/DTOs/Stats/V3/RelationshipStatV3.cs +++ b/Kavita.Models/DTOs/Stats/V3/RelationshipStatV3.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Stats.V3; +namespace Kavita.Models.DTOs.Stats.V3; /// /// KavitaStats - Information about Series Relationships diff --git a/API/DTOs/Stats/V3/ServerInfoV3Dto.cs b/Kavita.Models/DTOs/Stats/V3/ServerInfoV3Dto.cs similarity index 98% rename from API/DTOs/Stats/V3/ServerInfoV3Dto.cs rename to Kavita.Models/DTOs/Stats/V3/ServerInfoV3Dto.cs index 464179ca7..5614593b9 100644 --- a/API/DTOs/Stats/V3/ServerInfoV3Dto.cs +++ b/Kavita.Models/DTOs/Stats/V3/ServerInfoV3Dto.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.DTOs.Stats.V3; +namespace Kavita.Models.DTOs.Stats.V3; /// /// Represents information about a Kavita Installation for Kavita Stats v3 API diff --git a/API/DTOs/Stats/V3/UserStatV3.cs b/Kavita.Models/DTOs/Stats/V3/UserStatV3.cs similarity index 95% rename from API/DTOs/Stats/V3/UserStatV3.cs rename to Kavita.Models/DTOs/Stats/V3/UserStatV3.cs index de04f4113..8bad2b6db 100644 --- a/API/DTOs/Stats/V3/UserStatV3.cs +++ b/Kavita.Models/DTOs/Stats/V3/UserStatV3.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; -using API.Data.Misc; -using API.Entities.Enums; -using API.Entities.Enums.Device; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.Device; -namespace API.DTOs.Stats.V3; +namespace Kavita.Models.DTOs.Stats.V3; public sealed record UserStatV3 { diff --git a/API/DTOs/System/DirectoryDto.cs b/Kavita.Models/DTOs/System/DirectoryDto.cs similarity index 87% rename from API/DTOs/System/DirectoryDto.cs rename to Kavita.Models/DTOs/System/DirectoryDto.cs index 3b1408f7f..e0a5ea96c 100644 --- a/API/DTOs/System/DirectoryDto.cs +++ b/Kavita.Models/DTOs/System/DirectoryDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.System; +namespace Kavita.Models.DTOs.System; public sealed record DirectoryDto { diff --git a/API/DTOs/TachiyomiChapterDto.cs b/Kavita.Models/DTOs/TachiyomiChapterDto.cs similarity index 91% rename from API/DTOs/TachiyomiChapterDto.cs rename to Kavita.Models/DTOs/TachiyomiChapterDto.cs index ecdd5115c..91d5b7969 100644 --- a/API/DTOs/TachiyomiChapterDto.cs +++ b/Kavita.Models/DTOs/TachiyomiChapterDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable /// diff --git a/API/DTOs/Theme/ColorScapeDto.cs b/Kavita.Models/DTOs/Theme/ColorScapeDto.cs similarity index 91% rename from API/DTOs/Theme/ColorScapeDto.cs rename to Kavita.Models/DTOs/Theme/ColorScapeDto.cs index 2ebd96e2b..be035c1b2 100644 --- a/API/DTOs/Theme/ColorScapeDto.cs +++ b/Kavita.Models/DTOs/Theme/ColorScapeDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Theme; +namespace Kavita.Models.DTOs.Theme; #nullable enable /// diff --git a/API/DTOs/Theme/DownloadableSiteThemeDto.cs b/Kavita.Models/DTOs/Theme/DownloadableSiteThemeDto.cs similarity index 97% rename from API/DTOs/Theme/DownloadableSiteThemeDto.cs rename to Kavita.Models/DTOs/Theme/DownloadableSiteThemeDto.cs index 9f5991158..087f879fe 100644 --- a/API/DTOs/Theme/DownloadableSiteThemeDto.cs +++ b/Kavita.Models/DTOs/Theme/DownloadableSiteThemeDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Theme; +namespace Kavita.Models.DTOs.Theme; public sealed record DownloadableSiteThemeDto diff --git a/API/DTOs/Theme/SiteThemeDto.cs b/Kavita.Models/DTOs/Theme/SiteThemeDto.cs similarity index 95% rename from API/DTOs/Theme/SiteThemeDto.cs rename to Kavita.Models/DTOs/Theme/SiteThemeDto.cs index 7ae8369e9..6dc9d5695 100644 --- a/API/DTOs/Theme/SiteThemeDto.cs +++ b/Kavita.Models/DTOs/Theme/SiteThemeDto.cs @@ -1,8 +1,7 @@ using System.Collections.Generic; -using API.Entities.Enums.Theme; -using API.Services; +using Kavita.Models.Entities.Enums.Theme; -namespace API.DTOs.Theme; +namespace Kavita.Models.DTOs.Theme; /// /// Represents a set of css overrides the user can upload to Kavita and will load into webui diff --git a/API/DTOs/Theme/UpdateDefaultThemeDto.cs b/Kavita.Models/DTOs/Theme/UpdateDefaultThemeDto.cs similarity index 68% rename from API/DTOs/Theme/UpdateDefaultThemeDto.cs rename to Kavita.Models/DTOs/Theme/UpdateDefaultThemeDto.cs index aac0858c3..845dec922 100644 --- a/API/DTOs/Theme/UpdateDefaultThemeDto.cs +++ b/Kavita.Models/DTOs/Theme/UpdateDefaultThemeDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Theme; +namespace Kavita.Models.DTOs.Theme; public sealed record UpdateDefaultThemeDto { diff --git a/API/DTOs/Update/UpdateNotificationDto.cs b/Kavita.Models/DTOs/Update/UpdateNotificationDto.cs similarity index 98% rename from API/DTOs/Update/UpdateNotificationDto.cs rename to Kavita.Models/DTOs/Update/UpdateNotificationDto.cs index e1d2b81fe..67a5740f3 100644 --- a/API/DTOs/Update/UpdateNotificationDto.cs +++ b/Kavita.Models/DTOs/Update/UpdateNotificationDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs.Update; +namespace Kavita.Models.DTOs.Update; /// /// Update Notification denoting a new release available for user to update to diff --git a/API/DTOs/UpdateChapterDto.cs b/Kavita.Models/DTOs/UpdateChapterDto.cs similarity index 96% rename from API/DTOs/UpdateChapterDto.cs rename to Kavita.Models/DTOs/UpdateChapterDto.cs index 9ead8adc8..32fad623e 100644 --- a/API/DTOs/UpdateChapterDto.cs +++ b/Kavita.Models/DTOs/UpdateChapterDto.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; -using API.DTOs.Metadata; -using API.DTOs.Person; -using API.Entities.Enums; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Person; +using Kavita.Models.Entities.Enums; -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record UpdateChapterDto { diff --git a/API/DTOs/UpdateLibraryDto.cs b/Kavita.Models/DTOs/UpdateLibraryDto.cs similarity index 95% rename from API/DTOs/UpdateLibraryDto.cs rename to Kavita.Models/DTOs/UpdateLibraryDto.cs index ab7d01c36..96240a08b 100644 --- a/API/DTOs/UpdateLibraryDto.cs +++ b/Kavita.Models/DTOs/UpdateLibraryDto.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using API.Entities; -using API.Entities.Enums; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record UpdateLibraryDto { diff --git a/API/DTOs/UpdateLibraryForUserDto.cs b/Kavita.Models/DTOs/UpdateLibraryForUserDto.cs similarity index 88% rename from API/DTOs/UpdateLibraryForUserDto.cs rename to Kavita.Models/DTOs/UpdateLibraryForUserDto.cs index 4ce8d0df8..4b1c78fcf 100644 --- a/API/DTOs/UpdateLibraryForUserDto.cs +++ b/Kavita.Models/DTOs/UpdateLibraryForUserDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record UpdateLibraryForUserDto { diff --git a/API/DTOs/UpdateRBSDto.cs b/Kavita.Models/DTOs/UpdateRBSDto.cs similarity index 86% rename from API/DTOs/UpdateRBSDto.cs rename to Kavita.Models/DTOs/UpdateRBSDto.cs index fa8bb78f9..7615f4ea7 100644 --- a/API/DTOs/UpdateRBSDto.cs +++ b/Kavita.Models/DTOs/UpdateRBSDto.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable public sealed record UpdateRbsDto diff --git a/API/DTOs/UpdateRatingDto.cs b/Kavita.Models/DTOs/UpdateRatingDto.cs similarity index 83% rename from API/DTOs/UpdateRatingDto.cs rename to Kavita.Models/DTOs/UpdateRatingDto.cs index 472a94fe9..f0b756860 100644 --- a/API/DTOs/UpdateRatingDto.cs +++ b/Kavita.Models/DTOs/UpdateRatingDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record UpdateRatingDto { diff --git a/API/DTOs/UpdateSeriesDto.cs b/Kavita.Models/DTOs/UpdateSeriesDto.cs similarity index 90% rename from API/DTOs/UpdateSeriesDto.cs rename to Kavita.Models/DTOs/UpdateSeriesDto.cs index a4a9baf8c..6c99d0bd3 100644 --- a/API/DTOs/UpdateSeriesDto.cs +++ b/Kavita.Models/DTOs/UpdateSeriesDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable public sealed record UpdateSeriesDto diff --git a/API/DTOs/UpdateSeriesMetadataDto.cs b/Kavita.Models/DTOs/UpdateSeriesMetadataDto.cs similarity index 78% rename from API/DTOs/UpdateSeriesMetadataDto.cs rename to Kavita.Models/DTOs/UpdateSeriesMetadataDto.cs index 5225f5486..c9e9783ee 100644 --- a/API/DTOs/UpdateSeriesMetadataDto.cs +++ b/Kavita.Models/DTOs/UpdateSeriesMetadataDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record UpdateSeriesMetadataDto { diff --git a/API/DTOs/Uploads/UploadFileDto.cs b/Kavita.Models/DTOs/Uploads/UploadFileDto.cs similarity index 90% rename from API/DTOs/Uploads/UploadFileDto.cs rename to Kavita.Models/DTOs/Uploads/UploadFileDto.cs index 8d5cdf4cb..29608fbd0 100644 --- a/API/DTOs/Uploads/UploadFileDto.cs +++ b/Kavita.Models/DTOs/Uploads/UploadFileDto.cs @@ -1,4 +1,4 @@ -namespace API.DTOs.Uploads; +namespace Kavita.Models.DTOs.Uploads; public sealed record UploadFileDto { diff --git a/API/DTOs/Uploads/UploadUrlDto.cs b/Kavita.Models/DTOs/Uploads/UploadUrlDto.cs similarity index 84% rename from API/DTOs/Uploads/UploadUrlDto.cs rename to Kavita.Models/DTOs/Uploads/UploadUrlDto.cs index 3f4e625c3..1860068c2 100644 --- a/API/DTOs/Uploads/UploadUrlDto.cs +++ b/Kavita.Models/DTOs/Uploads/UploadUrlDto.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace API.DTOs.Uploads; +namespace Kavita.Models.DTOs.Uploads; public sealed record UploadUrlDto { diff --git a/API/DTOs/UserDto.cs b/Kavita.Models/DTOs/UserDto.cs similarity index 83% rename from API/DTOs/UserDto.cs rename to Kavita.Models/DTOs/UserDto.cs index ee32802f0..f948aa5c4 100644 --- a/API/DTOs/UserDto.cs +++ b/Kavita.Models/DTOs/UserDto.cs @@ -1,14 +1,15 @@  using System; using System.Collections.Generic; -using API.DTOs.Account; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Enums.User; -using API.Entities.Interfaces; +using Kavita.Models.DTOs.Account; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.User; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.User; using NotImplementedException = System.NotImplementedException; -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable public sealed record UserDto : IHasCoverImage diff --git a/API/DTOs/UserPreferencesDto.cs b/Kavita.Models/DTOs/UserPreferencesDto.cs similarity index 93% rename from API/DTOs/UserPreferencesDto.cs rename to Kavita.Models/DTOs/UserPreferencesDto.cs index 319438fef..244c74843 100644 --- a/API/DTOs/UserPreferencesDto.cs +++ b/Kavita.Models/DTOs/UserPreferencesDto.cs @@ -1,11 +1,11 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using API.DTOs.Theme; -using API.Entities; -using API.Entities.Enums.UserPreferences; -using API.Entities.User; +using Kavita.Models.DTOs.Theme; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.User; -namespace API.DTOs; +namespace Kavita.Models.DTOs; #nullable enable public sealed record UserPreferencesDto diff --git a/API/DTOs/UserReadingProfileDto.cs b/Kavita.Models/DTOs/UserReadingProfileDto.cs similarity index 96% rename from API/DTOs/UserReadingProfileDto.cs rename to Kavita.Models/DTOs/UserReadingProfileDto.cs index 1e59a5e85..484194af6 100644 --- a/API/DTOs/UserReadingProfileDto.cs +++ b/Kavita.Models/DTOs/UserReadingProfileDto.cs @@ -1,10 +1,11 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Enums.UserPreferences; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.User; -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record UserReadingProfileDto { diff --git a/API/DTOs/VolumeDto.cs b/Kavita.Models/DTOs/VolumeDto.cs similarity index 83% rename from API/DTOs/VolumeDto.cs rename to Kavita.Models/DTOs/VolumeDto.cs index b2c56ae0b..80c422289 100644 --- a/API/DTOs/VolumeDto.cs +++ b/Kavita.Models/DTOs/VolumeDto.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; -using API.Entities.Interfaces; -using API.Extensions; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Models.Entities.Interfaces; -namespace API.DTOs; +namespace Kavita.Models.DTOs; public sealed record VolumeDto : IHasReadTimeEstimate, IHasCoverImage { @@ -49,24 +47,6 @@ public sealed record VolumeDto : IHasReadTimeEstimate, IHasCoverImage public float AvgHoursToRead { get; set; } public long WordCount { get; set; } - /// - /// Is this a loose leaf volume - /// - /// - public bool IsLooseLeaf() - { - return MinNumber.Is(Parser.LooseLeafVolumeNumber); - } - - /// - /// Does this volume hold only specials - /// - /// - public bool IsSpecial() - { - return MinNumber.Is(Parser.SpecialVolumeNumber); - } - /// public string CoverImage { get; set; } /// diff --git a/API/DTOs/WantToRead/UpdateWantToReadDto.cs b/Kavita.Models/DTOs/WantToRead/UpdateWantToReadDto.cs similarity index 89% rename from API/DTOs/WantToRead/UpdateWantToReadDto.cs rename to Kavita.Models/DTOs/WantToRead/UpdateWantToReadDto.cs index a5be26857..a82d49f46 100644 --- a/API/DTOs/WantToRead/UpdateWantToReadDto.cs +++ b/Kavita.Models/DTOs/WantToRead/UpdateWantToReadDto.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace API.DTOs.WantToRead; +namespace Kavita.Models.DTOs.WantToRead; /// /// A list of Series to pass when working with Want To Read APIs diff --git a/Kavita.Models/Defaults.cs b/Kavita.Models/Defaults.cs new file mode 100644 index 000000000..e0da65b9a --- /dev/null +++ b/Kavita.Models/Defaults.cs @@ -0,0 +1,253 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.Font; +using Kavita.Models.Entities.Enums.User; +using Kavita.Models.Entities.User; + +namespace Kavita.Models; + +public static class Defaults +{ + public static readonly string DefaultFont = "Default"; + + /// + /// Generated on Startup. Seed.SeedSettings must run before + /// + public static ImmutableArray DefaultSettings; + + public static readonly ImmutableArray DefaultHighlightSlots = + [ + new() + { + Id = 1, + SlotNumber = 0, + Color = new RgbaColor { R = 0, G = 255, B = 255, A = 0.4f } + }, + new() + { + Id = 2, + SlotNumber = 1, + Color = new RgbaColor { R = 0, G = 255, B = 0, A = 0.4f } + }, + new() + { + Id = 3, + SlotNumber = 2, + Color = new RgbaColor { R = 255, G = 255, B = 0, A = 0.4f } + }, + new() + { + Id = 4, + SlotNumber = 3, + Color = new RgbaColor { R = 255, G = 165, B = 0, A = 0.4f } + }, + new() + { + Id = 5, + SlotNumber = 4, + Color = new RgbaColor { R = 255, G = 0, B = 255, A = 0.4f } + } + ]; + + public static readonly ImmutableArray DefaultFonts = + [ + new () + { + Name = DefaultFont, + NormalizedName = DefaultFont.ToNormalized(), + Provider = FontProvider.System, + FileName = string.Empty, + }, + new () + { + Name = "Merriweather", + NormalizedName = "Merriweather".ToNormalized(), + Provider = FontProvider.System, + FileName = "Merriweather-Regular.woff2", + }, + new () + { + Name = "EB Garamond", + NormalizedName = "EB Garamond".ToNormalized(), + Provider = FontProvider.System, + FileName = "EBGaramond-VariableFont_wght.woff2", + }, + new () + { + Name = "Fira Sans", + NormalizedName = "Fira Sans".ToNormalized(), + Provider = FontProvider.System, + FileName = "FiraSans-Regular.woff2", + }, + new () + { + Name = "Lato", + NormalizedName = "Lato".ToNormalized(), + Provider = FontProvider.System, + FileName = "Lato-Regular.woff2", + }, + new () + { + Name = "Libre Baskerville", + NormalizedName = "Libre Baskerville".ToNormalized(), + Provider = FontProvider.System, + FileName = "LibreBaskerville-Regular.woff2", + }, + new () + { + Name = "Nanum Gothic", + NormalizedName = ("Nanum Gothic").ToNormalized(), + Provider = FontProvider.System, + FileName = "NanumGothic-Regular.woff2", + }, + new () + { + Name = "Open Dyslexic", + NormalizedName = ("Open Dyslexic").ToNormalized(), + Provider = FontProvider.System, + FileName = "OpenDyslexic-Regular.woff2", + }, + new () + { + Name = "RocknRoll One", + NormalizedName = ("RocknRoll One").ToNormalized(), + Provider = FontProvider.System, + FileName = "RocknRollOne-Regular.woff2", + }, + new () + { + Name = "Fast Font Serif", + NormalizedName = ("Fast Font Serif").ToNormalized(), + Provider = FontProvider.System, + FileName = "Fast_Serif.woff2", + }, + new () + { + Name = "Fast Font Sans", + NormalizedName = ("Fast Font Sans").ToNormalized(), + Provider = FontProvider.System, + FileName = "Fast_Sans.woff2", + } + ]; + + public static readonly ImmutableArray DefaultThemes = [ + ..new List + { + SiteTheme.DefaultTheme, + }.ToArray() + ]; + + public static readonly ImmutableArray DefaultStreams = [ + ..new List + { + new() + { + Name = "on-deck", + StreamType = DashboardStreamType.OnDeck, + Order = 0, + IsProvided = true, + Visible = true + }, + new() + { + Name = "recently-updated", + StreamType = DashboardStreamType.RecentlyUpdated, + Order = 1, + IsProvided = true, + Visible = true + }, + new() + { + Name = "newly-added", + StreamType = DashboardStreamType.NewlyAdded, + Order = 2, + IsProvided = true, + Visible = true + }, + new() + { + Name = "more-in-genre", + StreamType = DashboardStreamType.MoreInGenre, + Order = 3, + IsProvided = true, + Visible = false + }, + }.ToArray() + ]; + + public static readonly ImmutableArray DefaultSideNavStreams = + [ + new() + { + Name = "want-to-read", + StreamType = SideNavStreamType.WantToRead, + Order = 1, + IsProvided = true, + Visible = true + }, new() + { + Name = "collections", + StreamType = SideNavStreamType.Collections, + Order = 2, + IsProvided = true, + Visible = true + }, new() + { + Name = "reading-lists", + StreamType = SideNavStreamType.ReadingLists, + Order = 3, + IsProvided = true, + Visible = true + }, new() + { + Name = "bookmarks", + StreamType = SideNavStreamType.Bookmarks, + Order = 4, + IsProvided = true, + Visible = true + }, new() + { + Name = "all-series", + StreamType = SideNavStreamType.AllSeries, + Order = 5, + IsProvided = true, + Visible = true + }, + new() + { + Name = "browse-authors", + StreamType = SideNavStreamType.BrowsePeople, + Order = 6, + IsProvided = true, + Visible = true + } + ]; + + public static List CreateDefaultAuthKeys() + { + return + [ + new AppUserAuthKey() + { + Name = AuthKeyHelper.OpdsKeyName, + Key = AuthKeyHelper.GenerateKey(32), + CreatedAtUtc = DateTime.UtcNow, + ExpiresAtUtc = null, + Provider = AuthKeyProvider.System, + }, + new AppUserAuthKey() + { + Name = AuthKeyHelper.ImageOnlyKeyName, + Key = AuthKeyHelper.GenerateKey(32), + CreatedAtUtc = DateTime.UtcNow, + ExpiresAtUtc = null, + Provider = AuthKeyProvider.System, + } + ]; + } +} diff --git a/API/Data/Misc/AgeRestriction.cs b/Kavita.Models/Entities/AgeRestriction.cs similarity index 63% rename from API/Data/Misc/AgeRestriction.cs rename to Kavita.Models/Entities/AgeRestriction.cs index 90c3c5888..72c2ce705 100644 --- a/API/Data/Misc/AgeRestriction.cs +++ b/Kavita.Models/Entities/AgeRestriction.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.Data.Misc; +namespace Kavita.Models.Entities; public class AgeRestriction { diff --git a/API/Entities/Chapter.cs b/Kavita.Models/Entities/Chapter.cs similarity index 77% rename from API/Entities/Chapter.cs rename to Kavita.Models/Entities/Chapter.cs index e4cd398ea..fdb6f478e 100644 --- a/API/Entities/Chapter.cs +++ b/Kavita.Models/Entities/Chapter.cs @@ -1,17 +1,15 @@ using System; using System.Collections.Generic; using System.Globalization; -using API.Entities.Enums; -using API.Entities.Interfaces; -using API.Entities.Metadata; -using API.Entities.MetadataMatching; -using API.Entities.Person; -using API.Entities.Progress; -using API.Entities.User; -using API.Extensions; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.MetadataMatching; +using Kavita.Models.Entities.Person; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; -namespace API.Entities; +namespace Kavita.Models.Entities; public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage, IHasKPlusMetadata { @@ -185,66 +183,6 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate, IHasCoverImage, IHasKP public ICollection ExternalReviews { get; set; } = []; public ICollection ExternalRatings { get; set; } = null!; - - public void UpdateFrom(ParserInfo info) - { - Files ??= new List(); - IsSpecial = info.IsSpecialInfo(); - if (IsSpecial) - { - Number = Parser.DefaultChapter; - MinNumber = Parser.DefaultChapterNumber; - MaxNumber = Parser.DefaultChapterNumber; - } - Title = (IsSpecial && info.Format is MangaFormat.Epub or MangaFormat.Pdf) - ? info.Title - : Parser.RemoveExtensionIfSupported(Range); - - var specialTreatment = info.IsSpecialInfo(); - Range = specialTreatment ? info.Filename : info.Chapters; - } - - /// - /// Returns the Chapter Number. If the chapter is a range, returns that, formatted. - /// - /// - public string GetNumberTitle() - { - try - { - if (MinNumber.Is(MaxNumber)) - { - if (MinNumber.Is(Parser.DefaultChapterNumber) && IsSpecial) - { - return Parser.RemoveExtensionIfSupported(Title); - } - - if (MinNumber.Is(0f) && !float.TryParse(Range, CultureInfo.InvariantCulture, out _)) - { - return $"{Range.ToString(CultureInfo.InvariantCulture)}"; - } - - return $"{MinNumber.ToString(CultureInfo.InvariantCulture)}"; - - } - - return $"{MinNumber.ToString(CultureInfo.InvariantCulture)}-{MaxNumber.ToString(CultureInfo.InvariantCulture)}"; - } - catch (Exception) - { - return MinNumber.ToString(CultureInfo.InvariantCulture); - } - } - - /// - /// Is the Chapter representing a single Volume (volume 1.cbz). If so, Min/Max will be Default and will not be special - /// - /// - public bool IsSingleVolumeChapter() - { - return MinNumber.Is(Parser.DefaultChapterNumber) && !IsSpecial; - } - public void ResetColorScape() { PrimaryColor = string.Empty; diff --git a/API/Entities/CollectionTag.cs b/Kavita.Models/Entities/CollectionTag.cs similarity index 96% rename from API/Entities/CollectionTag.cs rename to Kavita.Models/Entities/CollectionTag.cs index e23d0154c..0284ca7b3 100644 --- a/API/Entities/CollectionTag.cs +++ b/Kavita.Models/Entities/CollectionTag.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; -using API.Entities.Metadata; +using Kavita.Models.Entities.Metadata; using Microsoft.EntityFrameworkCore; -namespace API.Entities; +namespace Kavita.Models.Entities; /// /// Represents a user entered field that is used as a tagging and grouping mechanism diff --git a/API/Entities/Device.cs b/Kavita.Models/Entities/Device.cs similarity index 90% rename from API/Entities/Device.cs rename to Kavita.Models/Entities/Device.cs index 2f2ffa8c0..b5568860b 100644 --- a/API/Entities/Device.cs +++ b/Kavita.Models/Entities/Device.cs @@ -1,8 +1,9 @@ using System; -using API.Entities.Enums.Device; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.Enums.Device; +using Kavita.Models.Entities.User; -namespace API.Entities; +namespace Kavita.Models.Entities; /// /// A Device is an entity that can receive data from Kavita (kindle) diff --git a/API/Entities/EmailHistory.cs b/Kavita.Models/Entities/EmailHistory.cs similarity index 88% rename from API/Entities/EmailHistory.cs rename to Kavita.Models/Entities/EmailHistory.cs index f1ab95ca5..59bf5f6b4 100644 --- a/API/Entities/EmailHistory.cs +++ b/Kavita.Models/Entities/EmailHistory.cs @@ -1,8 +1,9 @@ using System; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; -namespace API.Entities; +namespace Kavita.Models.Entities; /// /// Records all emails that are sent from Kavita diff --git a/API/Entities/Enums/AgeRating.cs b/Kavita.Models/Entities/Enums/AgeRating.cs similarity index 96% rename from API/Entities/Enums/AgeRating.cs rename to Kavita.Models/Entities/Enums/AgeRating.cs index 9eefb9fa7..109fc22b0 100644 --- a/API/Entities/Enums/AgeRating.cs +++ b/Kavita.Models/Entities/Enums/AgeRating.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; /// /// Represents Age Rating for content. diff --git a/API/Entities/Enums/BookPageLayoutMode.cs b/Kavita.Models/Entities/Enums/BookPageLayoutMode.cs similarity index 83% rename from API/Entities/Enums/BookPageLayoutMode.cs rename to Kavita.Models/Entities/Enums/BookPageLayoutMode.cs index dc61b5a1e..3dfc12eaa 100644 --- a/API/Entities/Enums/BookPageLayoutMode.cs +++ b/Kavita.Models/Entities/Enums/BookPageLayoutMode.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum BookPageLayoutMode { diff --git a/API/Entities/Enums/ClientDevicePlatform.cs b/Kavita.Models/Entities/Enums/ClientDevicePlatform.cs similarity index 89% rename from API/Entities/Enums/ClientDevicePlatform.cs rename to Kavita.Models/Entities/Enums/ClientDevicePlatform.cs index d78c0171c..31154d500 100644 --- a/API/Entities/Enums/ClientDevicePlatform.cs +++ b/Kavita.Models/Entities/Enums/ClientDevicePlatform.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum ClientDevicePlatform { diff --git a/API/Entities/Enums/ClientDeviceType.cs b/Kavita.Models/Entities/Enums/ClientDeviceType.cs similarity index 93% rename from API/Entities/Enums/ClientDeviceType.cs rename to Kavita.Models/Entities/Enums/ClientDeviceType.cs index 9ab694043..5aed7a0e5 100644 --- a/API/Entities/Enums/ClientDeviceType.cs +++ b/Kavita.Models/Entities/Enums/ClientDeviceType.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum ClientDeviceType { diff --git a/API/Entities/Enums/CoverImageSize.cs b/Kavita.Models/Entities/Enums/CoverImageSize.cs similarity index 94% rename from API/Entities/Enums/CoverImageSize.cs rename to Kavita.Models/Entities/Enums/CoverImageSize.cs index d2d0eebb6..955eb5d07 100644 --- a/API/Entities/Enums/CoverImageSize.cs +++ b/Kavita.Models/Entities/Enums/CoverImageSize.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum CoverImageSize { diff --git a/API/Entities/Enums/DashboardStreamType.cs b/Kavita.Models/Entities/Enums/DashboardStreamType.cs similarity index 82% rename from API/Entities/Enums/DashboardStreamType.cs rename to Kavita.Models/Entities/Enums/DashboardStreamType.cs index 27a7d67ca..43f293741 100644 --- a/API/Entities/Enums/DashboardStreamType.cs +++ b/Kavita.Models/Entities/Enums/DashboardStreamType.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum DashboardStreamType { diff --git a/API/Entities/Enums/Device/DevicePlatform.cs b/Kavita.Models/Entities/Enums/Device/DevicePlatform.cs similarity index 91% rename from API/Entities/Enums/Device/DevicePlatform.cs rename to Kavita.Models/Entities/Enums/Device/DevicePlatform.cs index 41e68584b..a9999c402 100644 --- a/API/Entities/Enums/Device/DevicePlatform.cs +++ b/Kavita.Models/Entities/Enums/Device/DevicePlatform.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums.Device; +namespace Kavita.Models.Entities.Enums.Device; public enum EmailDevicePlatform { diff --git a/API/Entities/Enums/EncodeFormat.cs b/Kavita.Models/Entities/Enums/EncodeFormat.cs similarity index 81% rename from API/Entities/Enums/EncodeFormat.cs rename to Kavita.Models/Entities/Enums/EncodeFormat.cs index 70345f1db..4a4810788 100644 --- a/API/Entities/Enums/EncodeFormat.cs +++ b/Kavita.Models/Entities/Enums/EncodeFormat.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum EncodeFormat { diff --git a/API/Entities/Enums/EpubPageCalculationMethod.cs b/Kavita.Models/Entities/Enums/EpubPageCalculationMethod.cs similarity index 88% rename from API/Entities/Enums/EpubPageCalculationMethod.cs rename to Kavita.Models/Entities/Enums/EpubPageCalculationMethod.cs index 1fd2cb1de..42123d83b 100644 --- a/API/Entities/Enums/EpubPageCalculationMethod.cs +++ b/Kavita.Models/Entities/Enums/EpubPageCalculationMethod.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; /// /// Due to a bleeding text bug in the Epub reader with 1/2 column layout, multiple calculation modes are present diff --git a/API/Entities/Enums/FileTypeGroup.cs b/Kavita.Models/Entities/Enums/FileTypeGroup.cs similarity index 88% rename from API/Entities/Enums/FileTypeGroup.cs rename to Kavita.Models/Entities/Enums/FileTypeGroup.cs index eda039fc9..e3932ae73 100644 --- a/API/Entities/Enums/FileTypeGroup.cs +++ b/Kavita.Models/Entities/Enums/FileTypeGroup.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; /// /// Represents a set of file types that can be scanned diff --git a/API/Entities/Enums/Font/FontProvider.cs b/Kavita.Models/Entities/Enums/Font/FontProvider.cs similarity index 82% rename from API/Entities/Enums/Font/FontProvider.cs rename to Kavita.Models/Entities/Enums/Font/FontProvider.cs index ee944844a..4c79a52cc 100644 --- a/API/Entities/Enums/Font/FontProvider.cs +++ b/Kavita.Models/Entities/Enums/Font/FontProvider.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Enums.Font; +namespace Kavita.Models.Entities.Enums.Font; public enum FontProvider { diff --git a/API/Entities/Enums/IdentityProvider.cs b/Kavita.Models/Entities/Enums/IdentityProvider.cs similarity index 85% rename from API/Entities/Enums/IdentityProvider.cs rename to Kavita.Models/Entities/Enums/IdentityProvider.cs index 8ae814882..3c691fb5d 100644 --- a/API/Entities/Enums/IdentityProvider.cs +++ b/Kavita.Models/Entities/Enums/IdentityProvider.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; /// /// Who provides the identity of the user diff --git a/API/Entities/Enums/LayoutMode.cs b/Kavita.Models/Entities/Enums/LayoutMode.cs similarity index 83% rename from API/Entities/Enums/LayoutMode.cs rename to Kavita.Models/Entities/Enums/LayoutMode.cs index 37fc69293..21dfa2b6b 100644 --- a/API/Entities/Enums/LayoutMode.cs +++ b/Kavita.Models/Entities/Enums/LayoutMode.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum LayoutMode { diff --git a/API/Entities/Enums/LibraryType.cs b/Kavita.Models/Entities/Enums/LibraryType.cs similarity index 96% rename from API/Entities/Enums/LibraryType.cs rename to Kavita.Models/Entities/Enums/LibraryType.cs index 2e2bd235b..542587928 100644 --- a/API/Entities/Enums/LibraryType.cs +++ b/Kavita.Models/Entities/Enums/LibraryType.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum LibraryType { diff --git a/API/Entities/Enums/MangaFormat.cs b/Kavita.Models/Entities/Enums/MangaFormat.cs similarity index 95% rename from API/Entities/Enums/MangaFormat.cs rename to Kavita.Models/Entities/Enums/MangaFormat.cs index 26f744b9b..7f269cdc8 100644 --- a/API/Entities/Enums/MangaFormat.cs +++ b/Kavita.Models/Entities/Enums/MangaFormat.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; /// /// Represents the format of the file diff --git a/Kavita.Models/Entities/Enums/MediaErrorProducer.cs b/Kavita.Models/Entities/Enums/MediaErrorProducer.cs new file mode 100644 index 000000000..aed11169f --- /dev/null +++ b/Kavita.Models/Entities/Enums/MediaErrorProducer.cs @@ -0,0 +1,7 @@ +namespace Kavita.Models.Entities.Enums; + +public enum MediaErrorProducer +{ + BookService = 0, + ArchiveService = 1 +} diff --git a/API/Entities/Enums/MetadataFieldType.cs b/Kavita.Models/Entities/Enums/MetadataFieldType.cs similarity index 59% rename from API/Entities/Enums/MetadataFieldType.cs rename to Kavita.Models/Entities/Enums/MetadataFieldType.cs index 0052b6599..e0dc39561 100644 --- a/API/Entities/Enums/MetadataFieldType.cs +++ b/Kavita.Models/Entities/Enums/MetadataFieldType.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum MetadataFieldType { diff --git a/API/Entities/Enums/PageSplitOption.cs b/Kavita.Models/Entities/Enums/PageSplitOption.cs similarity index 73% rename from API/Entities/Enums/PageSplitOption.cs rename to Kavita.Models/Entities/Enums/PageSplitOption.cs index 7b421240c..864f5adb3 100644 --- a/API/Entities/Enums/PageSplitOption.cs +++ b/Kavita.Models/Entities/Enums/PageSplitOption.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum PageSplitOption { diff --git a/API/Entities/Enums/PdfRenderResolution.cs b/Kavita.Models/Entities/Enums/PdfRenderResolution.cs similarity index 86% rename from API/Entities/Enums/PdfRenderResolution.cs rename to Kavita.Models/Entities/Enums/PdfRenderResolution.cs index b6d0fec93..84f31ea50 100644 --- a/API/Entities/Enums/PdfRenderResolution.cs +++ b/Kavita.Models/Entities/Enums/PdfRenderResolution.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum PdfRenderResolution { diff --git a/API/Entities/Enums/PdfRenderResolutionExtensions.cs b/Kavita.Models/Entities/Enums/PdfRenderResolutionExtensions.cs similarity index 90% rename from API/Entities/Enums/PdfRenderResolutionExtensions.cs rename to Kavita.Models/Entities/Enums/PdfRenderResolutionExtensions.cs index cadb82abc..4d721cc1c 100644 --- a/API/Entities/Enums/PdfRenderResolutionExtensions.cs +++ b/Kavita.Models/Entities/Enums/PdfRenderResolutionExtensions.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public static class PdfRenderResolutionExtensions { diff --git a/API/Entities/Enums/PersonRole.cs b/Kavita.Models/Entities/Enums/PersonRole.cs similarity index 93% rename from API/Entities/Enums/PersonRole.cs rename to Kavita.Models/Entities/Enums/PersonRole.cs index f7ad45021..5519f2add 100644 --- a/API/Entities/Enums/PersonRole.cs +++ b/Kavita.Models/Entities/Enums/PersonRole.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum PersonRole { diff --git a/API/Entities/Enums/PublicationStatus.cs b/Kavita.Models/Entities/Enums/PublicationStatus.cs similarity index 95% rename from API/Entities/Enums/PublicationStatus.cs rename to Kavita.Models/Entities/Enums/PublicationStatus.cs index 614bc0604..542e08919 100644 --- a/API/Entities/Enums/PublicationStatus.cs +++ b/Kavita.Models/Entities/Enums/PublicationStatus.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum PublicationStatus { diff --git a/API/Entities/Enums/RatingAuthority.cs b/Kavita.Models/Entities/Enums/RatingAuthority.cs similarity index 88% rename from API/Entities/Enums/RatingAuthority.cs rename to Kavita.Models/Entities/Enums/RatingAuthority.cs index 0f358a9a7..2bb0cc860 100644 --- a/API/Entities/Enums/RatingAuthority.cs +++ b/Kavita.Models/Entities/Enums/RatingAuthority.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum RatingAuthority { diff --git a/API/Entities/Enums/ReaderMode.cs b/Kavita.Models/Entities/Enums/ReaderMode.cs similarity index 84% rename from API/Entities/Enums/ReaderMode.cs rename to Kavita.Models/Entities/Enums/ReaderMode.cs index e1353ad59..cc85911f2 100644 --- a/API/Entities/Enums/ReaderMode.cs +++ b/Kavita.Models/Entities/Enums/ReaderMode.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum ReaderMode { diff --git a/API/Entities/Enums/ReadingDirection.cs b/Kavita.Models/Entities/Enums/ReadingDirection.cs similarity index 63% rename from API/Entities/Enums/ReadingDirection.cs rename to Kavita.Models/Entities/Enums/ReadingDirection.cs index 8804ca6d4..016e35e1b 100644 --- a/API/Entities/Enums/ReadingDirection.cs +++ b/Kavita.Models/Entities/Enums/ReadingDirection.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum ReadingDirection { diff --git a/API/Entities/Enums/ReadingProfileKind.cs b/Kavita.Models/Entities/Enums/ReadingProfileKind.cs similarity index 91% rename from API/Entities/Enums/ReadingProfileKind.cs rename to Kavita.Models/Entities/Enums/ReadingProfileKind.cs index 0f9cfa20b..b1b0b71e6 100644 --- a/API/Entities/Enums/ReadingProfileKind.cs +++ b/Kavita.Models/Entities/Enums/ReadingProfileKind.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum ReadingProfileKind { diff --git a/API/Entities/Enums/RelationKind.cs b/Kavita.Models/Entities/Enums/RelationKind.cs similarity index 98% rename from API/Entities/Enums/RelationKind.cs rename to Kavita.Models/Entities/Enums/RelationKind.cs index 61516ec0d..d04dacfbd 100644 --- a/API/Entities/Enums/RelationKind.cs +++ b/Kavita.Models/Entities/Enums/RelationKind.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; /// /// Represents a relationship between Series diff --git a/API/Entities/Enums/ScalingOption.cs b/Kavita.Models/Entities/Enums/ScalingOption.cs similarity index 71% rename from API/Entities/Enums/ScalingOption.cs rename to Kavita.Models/Entities/Enums/ScalingOption.cs index f0b357898..940442b38 100644 --- a/API/Entities/Enums/ScalingOption.cs +++ b/Kavita.Models/Entities/Enums/ScalingOption.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum ScalingOption { diff --git a/Kavita.Models/Entities/Enums/ScrobbleProvider.cs b/Kavita.Models/Entities/Enums/ScrobbleProvider.cs new file mode 100644 index 000000000..556244983 --- /dev/null +++ b/Kavita.Models/Entities/Enums/ScrobbleProvider.cs @@ -0,0 +1,20 @@ +using System; + +namespace Kavita.Models.Entities.Enums; + +/// +/// Misleading name but is the source of data (like a review coming from AniList) +/// +public enum ScrobbleProvider +{ + /// + /// For now, this means data comes from within this instance of Kavita + /// + Kavita = 0, + AniList = 1, + Mal = 2, + [Obsolete("No longer supported")] + GoogleBooks = 3, + Cbr = 4, + Hardcover = 5, +} diff --git a/API/Entities/Enums/ServerSettingKey.cs b/Kavita.Models/Entities/Enums/ServerSettingKey.cs similarity index 99% rename from API/Entities/Enums/ServerSettingKey.cs rename to Kavita.Models/Entities/Enums/ServerSettingKey.cs index 43fc63ed2..175048bc8 100644 --- a/API/Entities/Enums/ServerSettingKey.cs +++ b/Kavita.Models/Entities/Enums/ServerSettingKey.cs @@ -1,7 +1,7 @@ using System; using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; /// /// 15 is blocked as it was EnableSwaggerUi, which is no longer used diff --git a/API/Entities/Enums/SyncKey.cs b/Kavita.Models/Entities/Enums/SyncKey.cs similarity index 81% rename from API/Entities/Enums/SyncKey.cs rename to Kavita.Models/Entities/Enums/SyncKey.cs index 6e5346ab8..7e7c80a7d 100644 --- a/API/Entities/Enums/SyncKey.cs +++ b/Kavita.Models/Entities/Enums/SyncKey.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; public enum SyncKey { diff --git a/API/Entities/Enums/Theme/ThemeProvider.cs b/Kavita.Models/Entities/Enums/Theme/ThemeProvider.cs similarity index 88% rename from API/Entities/Enums/Theme/ThemeProvider.cs rename to Kavita.Models/Entities/Enums/Theme/ThemeProvider.cs index cc12a552e..e3dd64dc4 100644 --- a/API/Entities/Enums/Theme/ThemeProvider.cs +++ b/Kavita.Models/Entities/Enums/Theme/ThemeProvider.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums.Theme; +namespace Kavita.Models.Entities.Enums.Theme; public enum ThemeProvider { diff --git a/API/Entities/Enums/User/AuthKeyProvider.cs b/Kavita.Models/Entities/Enums/User/AuthKeyProvider.cs similarity index 86% rename from API/Entities/Enums/User/AuthKeyProvider.cs rename to Kavita.Models/Entities/Enums/User/AuthKeyProvider.cs index 4a2da9ada..578b22204 100644 --- a/API/Entities/Enums/User/AuthKeyProvider.cs +++ b/Kavita.Models/Entities/Enums/User/AuthKeyProvider.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums.User; +namespace Kavita.Models.Entities.Enums.User; public enum AuthKeyProvider { diff --git a/API/Entities/Enums/UserPreferences/AppUserOpdsPreferences.cs b/Kavita.Models/Entities/Enums/UserPreferences/AppUserOpdsPreferences.cs similarity index 87% rename from API/Entities/Enums/UserPreferences/AppUserOpdsPreferences.cs rename to Kavita.Models/Entities/Enums/UserPreferences/AppUserOpdsPreferences.cs index 5fdb2f5f4..347bbe07a 100644 --- a/API/Entities/Enums/UserPreferences/AppUserOpdsPreferences.cs +++ b/Kavita.Models/Entities/Enums/UserPreferences/AppUserOpdsPreferences.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Enums.UserPreferences; +namespace Kavita.Models.Entities.Enums.UserPreferences; public class AppUserOpdsPreferences { diff --git a/API/Entities/Enums/UserPreferences/AppUserSocialPreferences.cs b/Kavita.Models/Entities/Enums/UserPreferences/AppUserSocialPreferences.cs similarity index 96% rename from API/Entities/Enums/UserPreferences/AppUserSocialPreferences.cs rename to Kavita.Models/Entities/Enums/UserPreferences/AppUserSocialPreferences.cs index 4b873ebed..221a03026 100644 --- a/API/Entities/Enums/UserPreferences/AppUserSocialPreferences.cs +++ b/Kavita.Models/Entities/Enums/UserPreferences/AppUserSocialPreferences.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.Entities.Enums.UserPreferences; +namespace Kavita.Models.Entities.Enums.UserPreferences; public class AppUserSocialPreferences { diff --git a/API/Entities/Enums/UserPreferences/KeyBind.cs b/Kavita.Models/Entities/Enums/UserPreferences/KeyBind.cs similarity index 85% rename from API/Entities/Enums/UserPreferences/KeyBind.cs rename to Kavita.Models/Entities/Enums/UserPreferences/KeyBind.cs index 5a15ec5ec..d23cec012 100644 --- a/API/Entities/Enums/UserPreferences/KeyBind.cs +++ b/Kavita.Models/Entities/Enums/UserPreferences/KeyBind.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace API.Entities.Enums.UserPreferences; +namespace Kavita.Models.Entities.Enums.UserPreferences; #nullable enable public sealed record KeyBind diff --git a/API/Entities/Enums/UserPreferences/KeyBindTarget.cs b/Kavita.Models/Entities/Enums/UserPreferences/KeyBindTarget.cs similarity index 94% rename from API/Entities/Enums/UserPreferences/KeyBindTarget.cs rename to Kavita.Models/Entities/Enums/UserPreferences/KeyBindTarget.cs index 666f77c73..91c784617 100644 --- a/API/Entities/Enums/UserPreferences/KeyBindTarget.cs +++ b/Kavita.Models/Entities/Enums/UserPreferences/KeyBindTarget.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums.UserPreferences; +namespace Kavita.Models.Entities.Enums.UserPreferences; public enum KeyBindTarget { diff --git a/API/Entities/Enums/UserPreferences/PageLayoutMode.cs b/Kavita.Models/Entities/Enums/UserPreferences/PageLayoutMode.cs similarity index 72% rename from API/Entities/Enums/UserPreferences/PageLayoutMode.cs rename to Kavita.Models/Entities/Enums/UserPreferences/PageLayoutMode.cs index 328e90d35..182da3e34 100644 --- a/API/Entities/Enums/UserPreferences/PageLayoutMode.cs +++ b/Kavita.Models/Entities/Enums/UserPreferences/PageLayoutMode.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums.UserPreferences; +namespace Kavita.Models.Entities.Enums.UserPreferences; public enum PageLayoutMode { diff --git a/API/Entities/Enums/UserPreferences/PdfBookMode.cs b/Kavita.Models/Entities/Enums/UserPreferences/PdfBookMode.cs similarity index 89% rename from API/Entities/Enums/UserPreferences/PdfBookMode.cs rename to Kavita.Models/Entities/Enums/UserPreferences/PdfBookMode.cs index 5946e17c5..1a0c719a6 100644 --- a/API/Entities/Enums/UserPreferences/PdfBookMode.cs +++ b/Kavita.Models/Entities/Enums/UserPreferences/PdfBookMode.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums.UserPreferences; +namespace Kavita.Models.Entities.Enums.UserPreferences; public enum PdfLayoutMode { diff --git a/API/Entities/Enums/UserPreferences/PdfScrollMode.cs b/Kavita.Models/Entities/Enums/UserPreferences/PdfScrollMode.cs similarity index 87% rename from API/Entities/Enums/UserPreferences/PdfScrollMode.cs rename to Kavita.Models/Entities/Enums/UserPreferences/PdfScrollMode.cs index 93cc5bd2e..81c54bd7a 100644 --- a/API/Entities/Enums/UserPreferences/PdfScrollMode.cs +++ b/Kavita.Models/Entities/Enums/UserPreferences/PdfScrollMode.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums.UserPreferences; +namespace Kavita.Models.Entities.Enums.UserPreferences; /// /// Enum values match PdfViewer's enums diff --git a/API/Entities/Enums/UserPreferences/PdfSpreadMode.cs b/Kavita.Models/Entities/Enums/UserPreferences/PdfSpreadMode.cs similarity index 76% rename from API/Entities/Enums/UserPreferences/PdfSpreadMode.cs rename to Kavita.Models/Entities/Enums/UserPreferences/PdfSpreadMode.cs index 412239d4a..50f4bd99c 100644 --- a/API/Entities/Enums/UserPreferences/PdfSpreadMode.cs +++ b/Kavita.Models/Entities/Enums/UserPreferences/PdfSpreadMode.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums.UserPreferences; +namespace Kavita.Models.Entities.Enums.UserPreferences; public enum PdfSpreadMode { diff --git a/API/Entities/Enums/UserPreferences/PdfTheme.cs b/Kavita.Models/Entities/Enums/UserPreferences/PdfTheme.cs similarity index 71% rename from API/Entities/Enums/UserPreferences/PdfTheme.cs rename to Kavita.Models/Entities/Enums/UserPreferences/PdfTheme.cs index 0efe1dfde..8e4292cdf 100644 --- a/API/Entities/Enums/UserPreferences/PdfTheme.cs +++ b/Kavita.Models/Entities/Enums/UserPreferences/PdfTheme.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums.UserPreferences; +namespace Kavita.Models.Entities.Enums.UserPreferences; public enum PdfTheme { diff --git a/API/Entities/Enums/WritingStyle.cs b/Kavita.Models/Entities/Enums/WritingStyle.cs similarity index 91% rename from API/Entities/Enums/WritingStyle.cs rename to Kavita.Models/Entities/Enums/WritingStyle.cs index 37d50c160..0f515e12b 100644 --- a/API/Entities/Enums/WritingStyle.cs +++ b/Kavita.Models/Entities/Enums/WritingStyle.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace API.Entities.Enums; +namespace Kavita.Models.Entities.Enums; /// /// Represents the writing styles for the book-reader diff --git a/API/Entities/EpubFont.cs b/Kavita.Models/Entities/EpubFont.cs similarity index 85% rename from API/Entities/EpubFont.cs rename to Kavita.Models/Entities/EpubFont.cs index 0cf745db6..dc9373d02 100644 --- a/API/Entities/EpubFont.cs +++ b/Kavita.Models/Entities/EpubFont.cs @@ -1,9 +1,8 @@ using System; -using API.Entities.Enums.Font; -using API.Entities.Interfaces; -using API.Services; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.Enums.Font; -namespace API.Entities; +namespace Kavita.Models.Entities; /// /// Represents a user provider font to be used in the epub reader @@ -34,4 +33,6 @@ public class EpubFont: IEntityDate public DateTime CreatedUtc { get; set; } public DateTime LastModified { get; set; } public DateTime LastModifiedUtc { get; set; } + + public static readonly string DefaultFont = "Default"; } diff --git a/API/Entities/FolderPath.cs b/Kavita.Models/Entities/FolderPath.cs similarity index 95% rename from API/Entities/FolderPath.cs rename to Kavita.Models/Entities/FolderPath.cs index 2d5684ba9..4d0e31a2f 100644 --- a/API/Entities/FolderPath.cs +++ b/Kavita.Models/Entities/FolderPath.cs @@ -1,7 +1,7 @@  using System; -namespace API.Entities; +namespace Kavita.Models.Entities; public class FolderPath { diff --git a/API/Entities/Genre.cs b/Kavita.Models/Entities/Genre.cs similarity index 85% rename from API/Entities/Genre.cs rename to Kavita.Models/Entities/Genre.cs index 56cb446b2..5750bb471 100644 --- a/API/Entities/Genre.cs +++ b/Kavita.Models/Entities/Genre.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; -using API.Entities.Metadata; +using Kavita.Models.Entities.Metadata; using Microsoft.EntityFrameworkCore; -namespace API.Entities; +namespace Kavita.Models.Entities; [Index(nameof(NormalizedTitle), IsUnique = true)] public class Genre diff --git a/API/Entities/HighlightSlot.cs b/Kavita.Models/Entities/HighlightSlot.cs similarity index 89% rename from API/Entities/HighlightSlot.cs rename to Kavita.Models/Entities/HighlightSlot.cs index 2f951b290..4720b3871 100644 --- a/API/Entities/HighlightSlot.cs +++ b/Kavita.Models/Entities/HighlightSlot.cs @@ -1,4 +1,4 @@ -namespace API.Entities; +namespace Kavita.Models.Entities; public sealed record HighlightSlot { diff --git a/API/Entities/History/KavitaPlusHistory.cs b/Kavita.Models/Entities/History/KavitaPlusHistory.cs similarity index 73% rename from API/Entities/History/KavitaPlusHistory.cs rename to Kavita.Models/Entities/History/KavitaPlusHistory.cs index 81b7e5e40..faa8dbdee 100644 --- a/API/Entities/History/KavitaPlusHistory.cs +++ b/Kavita.Models/Entities/History/KavitaPlusHistory.cs @@ -1,4 +1,4 @@ -namespace API.Entities.History; +namespace Kavita.Models.Entities.History; /// /// Records history of actions Kavita+ takes diff --git a/API/Entities/History/ManualMigrationHistory.cs b/Kavita.Models/Entities/History/ManualMigrationHistory.cs similarity index 91% rename from API/Entities/History/ManualMigrationHistory.cs rename to Kavita.Models/Entities/History/ManualMigrationHistory.cs index 4e22d0f0c..34cce7d26 100644 --- a/API/Entities/History/ManualMigrationHistory.cs +++ b/Kavita.Models/Entities/History/ManualMigrationHistory.cs @@ -1,7 +1,7 @@ using System; using Kavita.Common.EnvironmentInfo; -namespace API.Entities.History; +namespace Kavita.Models.Entities.History; /// /// This will track manual migrations so that I can use simple selects to check if a Manual Migration is needed diff --git a/API/Entities/Interfaces/IEntityDate.cs b/Kavita.Models/Entities/Interfaces/IEntityDate.cs similarity index 82% rename from API/Entities/Interfaces/IEntityDate.cs rename to Kavita.Models/Entities/Interfaces/IEntityDate.cs index 3ffcebfd2..ca8aabc3c 100644 --- a/API/Entities/Interfaces/IEntityDate.cs +++ b/Kavita.Models/Entities/Interfaces/IEntityDate.cs @@ -1,6 +1,6 @@ using System; -namespace API.Entities.Interfaces; +namespace Kavita.Models.Entities.Interfaces; public interface IEntityDate { diff --git a/API/Entities/Interfaces/IHasConcurrencyToken.cs b/Kavita.Models/Entities/Interfaces/IHasConcurrencyToken.cs similarity index 89% rename from API/Entities/Interfaces/IHasConcurrencyToken.cs rename to Kavita.Models/Entities/Interfaces/IHasConcurrencyToken.cs index 3cd3f1adf..b469ca3e0 100644 --- a/API/Entities/Interfaces/IHasConcurrencyToken.cs +++ b/Kavita.Models/Entities/Interfaces/IHasConcurrencyToken.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Interfaces; +namespace Kavita.Models.Entities.Interfaces; /// /// An interface abstracting an entity that has a concurrency token. diff --git a/API/Entities/Interfaces/IHasCoverImage.cs b/Kavita.Models/Entities/Interfaces/IHasCoverImage.cs similarity index 93% rename from API/Entities/Interfaces/IHasCoverImage.cs rename to Kavita.Models/Entities/Interfaces/IHasCoverImage.cs index 5570e37eb..5696da153 100644 --- a/API/Entities/Interfaces/IHasCoverImage.cs +++ b/Kavita.Models/Entities/Interfaces/IHasCoverImage.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Interfaces; +namespace Kavita.Models.Entities.Interfaces; #nullable enable diff --git a/API/Entities/Interfaces/IHasKPlusMetadata.cs b/Kavita.Models/Entities/Interfaces/IHasKPlusMetadata.cs similarity index 71% rename from API/Entities/Interfaces/IHasKPlusMetadata.cs rename to Kavita.Models/Entities/Interfaces/IHasKPlusMetadata.cs index 062afd7e1..717372cb0 100644 --- a/API/Entities/Interfaces/IHasKPlusMetadata.cs +++ b/Kavita.Models/Entities/Interfaces/IHasKPlusMetadata.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using API.Entities.MetadataMatching; +using Kavita.Models.Entities.MetadataMatching; -namespace API.Entities.Interfaces; +namespace Kavita.Models.Entities.Interfaces; public interface IHasKPlusMetadata { diff --git a/API/Entities/Interfaces/IHasReadTimeEstimate.cs b/Kavita.Models/Entities/Interfaces/IHasReadTimeEstimate.cs similarity index 92% rename from API/Entities/Interfaces/IHasReadTimeEstimate.cs rename to Kavita.Models/Entities/Interfaces/IHasReadTimeEstimate.cs index 7816da054..f41c35509 100644 --- a/API/Entities/Interfaces/IHasReadTimeEstimate.cs +++ b/Kavita.Models/Entities/Interfaces/IHasReadTimeEstimate.cs @@ -1,6 +1,5 @@ -using API.Services.Reading; - -namespace API.Entities.Interfaces; + +namespace Kavita.Models.Entities.Interfaces; /// /// Entity has read time estimate properties to estimate time to read diff --git a/API/Entities/Interfaces/ITheme.cs b/Kavita.Models/Entities/Interfaces/ITheme.cs similarity index 76% rename from API/Entities/Interfaces/ITheme.cs rename to Kavita.Models/Entities/Interfaces/ITheme.cs index 216136569..c33565610 100644 --- a/API/Entities/Interfaces/ITheme.cs +++ b/Kavita.Models/Entities/Interfaces/ITheme.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums.Theme; +using Kavita.Models.Entities.Enums.Theme; -namespace API.Entities.Interfaces; +namespace Kavita.Models.Entities.Interfaces; /// /// A theme in some kind diff --git a/API/Entities/Library.cs b/Kavita.Models/Entities/Library.cs similarity index 96% rename from API/Entities/Library.cs rename to Kavita.Models/Entities/Library.cs index 66fd0eda9..0881a5171 100644 --- a/API/Entities/Library.cs +++ b/Kavita.Models/Entities/Library.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; -using API.Entities.Enums; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.User; -namespace API.Entities; +namespace Kavita.Models.Entities; public class Library : IEntityDate, IHasCoverImage { diff --git a/API/Entities/LibraryExcludedGlob.cs b/Kavita.Models/Entities/LibraryExcludedGlob.cs similarity index 84% rename from API/Entities/LibraryExcludedGlob.cs rename to Kavita.Models/Entities/LibraryExcludedGlob.cs index 69bc86342..9998ad63c 100644 --- a/API/Entities/LibraryExcludedGlob.cs +++ b/Kavita.Models/Entities/LibraryExcludedGlob.cs @@ -1,4 +1,4 @@ -namespace API.Entities; +namespace Kavita.Models.Entities; public class LibraryExcludePattern { diff --git a/API/Entities/LibraryFileTypeGroup.cs b/Kavita.Models/Entities/LibraryFileTypeGroup.cs similarity index 74% rename from API/Entities/LibraryFileTypeGroup.cs rename to Kavita.Models/Entities/LibraryFileTypeGroup.cs index a3af30d80..c75c55e54 100644 --- a/API/Entities/LibraryFileTypeGroup.cs +++ b/Kavita.Models/Entities/LibraryFileTypeGroup.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.Entities; +namespace Kavita.Models.Entities; public class LibraryFileTypeGroup { diff --git a/API/Entities/MangaFile.cs b/Kavita.Models/Entities/MangaFile.cs similarity index 95% rename from API/Entities/MangaFile.cs rename to Kavita.Models/Entities/MangaFile.cs index 2f1708226..909fcc312 100644 --- a/API/Entities/MangaFile.cs +++ b/Kavita.Models/Entities/MangaFile.cs @@ -1,10 +1,10 @@  using System; using System.IO; -using API.Entities.Enums; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; -namespace API.Entities; +namespace Kavita.Models.Entities; /// /// Represents a wrapper to the underlying file. This provides information around file, like number of pages, format, etc. diff --git a/API/Entities/MediaError.cs b/Kavita.Models/Entities/MediaError.cs similarity index 92% rename from API/Entities/MediaError.cs rename to Kavita.Models/Entities/MediaError.cs index 33e55ed8e..ecff2e2d5 100644 --- a/API/Entities/MediaError.cs +++ b/Kavita.Models/Entities/MediaError.cs @@ -1,7 +1,7 @@ using System; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Interfaces; -namespace API.Entities; +namespace Kavita.Models.Entities; /// /// Represents issues found during scanning or interacting with media. For example) Can't open file, corrupt media, missing content in epub. diff --git a/API/Entities/Metadata/ExternalRating.cs b/Kavita.Models/Entities/Metadata/ExternalRating.cs similarity index 89% rename from API/Entities/Metadata/ExternalRating.cs rename to Kavita.Models/Entities/Metadata/ExternalRating.cs index 7fc2b9353..b836c4094 100644 --- a/API/Entities/Metadata/ExternalRating.cs +++ b/Kavita.Models/Entities/Metadata/ExternalRating.cs @@ -1,8 +1,7 @@ using System.Collections.Generic; -using API.Entities.Enums; -using API.Services.Plus; +using Kavita.Models.Entities.Enums; -namespace API.Entities.Metadata; +namespace Kavita.Models.Entities.Metadata; public class ExternalRating { diff --git a/API/Entities/Metadata/ExternalRecommendation.cs b/Kavita.Models/Entities/Metadata/ExternalRecommendation.cs similarity index 91% rename from API/Entities/Metadata/ExternalRecommendation.cs rename to Kavita.Models/Entities/Metadata/ExternalRecommendation.cs index c5bb98f20..a5733af7e 100644 --- a/API/Entities/Metadata/ExternalRecommendation.cs +++ b/Kavita.Models/Entities/Metadata/ExternalRecommendation.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; -using API.Services.Plus; +using Kavita.Models.Entities.Enums; using Microsoft.EntityFrameworkCore; -namespace API.Entities.Metadata; +namespace Kavita.Models.Entities.Metadata; [Index(nameof(SeriesId), IsUnique = false)] public class ExternalRecommendation diff --git a/API/Entities/Metadata/ExternalReview.cs b/Kavita.Models/Entities/Metadata/ExternalReview.cs similarity index 93% rename from API/Entities/Metadata/ExternalReview.cs rename to Kavita.Models/Entities/Metadata/ExternalReview.cs index 73c71e5ee..55be48f9b 100644 --- a/API/Entities/Metadata/ExternalReview.cs +++ b/Kavita.Models/Entities/Metadata/ExternalReview.cs @@ -1,8 +1,7 @@ using System.Collections.Generic; -using API.Entities.Enums; -using API.Services.Plus; +using Kavita.Models.Entities.Enums; -namespace API.Entities.Metadata; +namespace Kavita.Models.Entities.Metadata; /// /// Represents an Externally supplied Review for a given Series diff --git a/API/Entities/Metadata/ExternalSeriesMetadata.cs b/Kavita.Models/Entities/Metadata/ExternalSeriesMetadata.cs similarity index 96% rename from API/Entities/Metadata/ExternalSeriesMetadata.cs rename to Kavita.Models/Entities/Metadata/ExternalSeriesMetadata.cs index 1ab37ba3c..2e31b3924 100644 --- a/API/Entities/Metadata/ExternalSeriesMetadata.cs +++ b/Kavita.Models/Entities/Metadata/ExternalSeriesMetadata.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace API.Entities.Metadata; +namespace Kavita.Models.Entities.Metadata; /// /// External Metadata from Kavita+ for a Series diff --git a/API/Entities/Metadata/SeriesBlacklist.cs b/Kavita.Models/Entities/Metadata/SeriesBlacklist.cs similarity index 89% rename from API/Entities/Metadata/SeriesBlacklist.cs rename to Kavita.Models/Entities/Metadata/SeriesBlacklist.cs index 3d262eeb4..c231ab6de 100644 --- a/API/Entities/Metadata/SeriesBlacklist.cs +++ b/Kavita.Models/Entities/Metadata/SeriesBlacklist.cs @@ -1,6 +1,6 @@ using System; -namespace API.Entities.Metadata; +namespace Kavita.Models.Entities.Metadata; /// /// A blacklist of Series for Kavita+ diff --git a/API/Entities/Metadata/SeriesMetadata.cs b/Kavita.Models/Entities/Metadata/SeriesMetadata.cs similarity index 96% rename from API/Entities/Metadata/SeriesMetadata.cs rename to Kavita.Models/Entities/Metadata/SeriesMetadata.cs index e304dee6c..f5fdad0c8 100644 --- a/API/Entities/Metadata/SeriesMetadata.cs +++ b/Kavita.Models/Entities/Metadata/SeriesMetadata.cs @@ -2,13 +2,13 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -using API.Entities.Enums; -using API.Entities.Interfaces; -using API.Entities.MetadataMatching; -using API.Entities.Person; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.MetadataMatching; +using Kavita.Models.Entities.Person; using Microsoft.EntityFrameworkCore; -namespace API.Entities.Metadata; +namespace Kavita.Models.Entities.Metadata; [Index(nameof(Id), nameof(SeriesId), IsUnique = true)] public class SeriesMetadata : IHasConcurrencyToken, IHasKPlusMetadata diff --git a/API/Entities/Metadata/SeriesRelation.cs b/Kavita.Models/Entities/Metadata/SeriesRelation.cs similarity index 87% rename from API/Entities/Metadata/SeriesRelation.cs rename to Kavita.Models/Entities/Metadata/SeriesRelation.cs index 7493f945b..08aea5cf5 100644 --- a/API/Entities/Metadata/SeriesRelation.cs +++ b/Kavita.Models/Entities/Metadata/SeriesRelation.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.Entities.Metadata; +namespace Kavita.Models.Entities.Metadata; /// /// A relation flows between one series and another. diff --git a/API/Entities/MetadataMatching/MetadataFieldMapping.cs b/Kavita.Models/Entities/MetadataMatching/MetadataFieldMapping.cs similarity index 85% rename from API/Entities/MetadataMatching/MetadataFieldMapping.cs rename to Kavita.Models/Entities/MetadataMatching/MetadataFieldMapping.cs index e7dd88c03..abf18203b 100644 --- a/API/Entities/MetadataMatching/MetadataFieldMapping.cs +++ b/Kavita.Models/Entities/MetadataMatching/MetadataFieldMapping.cs @@ -1,7 +1,7 @@ -using API.Entities.Enums; -using API.Entities.MetadataMatching; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.MetadataMatching; -namespace API.Entities; +namespace Kavita.Models.Entities; public class MetadataFieldMapping { diff --git a/API/Entities/MetadataMatching/MetadataSettingField.cs b/Kavita.Models/Entities/MetadataMatching/MetadataSettingField.cs similarity index 90% rename from API/Entities/MetadataMatching/MetadataSettingField.cs rename to Kavita.Models/Entities/MetadataMatching/MetadataSettingField.cs index 9333c269e..acc14313b 100644 --- a/API/Entities/MetadataMatching/MetadataSettingField.cs +++ b/Kavita.Models/Entities/MetadataMatching/MetadataSettingField.cs @@ -1,4 +1,4 @@ -namespace API.Entities.MetadataMatching; +namespace Kavita.Models.Entities.MetadataMatching; /// /// Represents which field that can be written to as an override when already locked diff --git a/API/Entities/MetadataMatching/MetadataSettings.cs b/Kavita.Models/Entities/MetadataMatching/MetadataSettings.cs similarity index 97% rename from API/Entities/MetadataMatching/MetadataSettings.cs rename to Kavita.Models/Entities/MetadataMatching/MetadataSettings.cs index b72329342..6d4caf64d 100644 --- a/API/Entities/MetadataMatching/MetadataSettings.cs +++ b/Kavita.Models/Entities/MetadataMatching/MetadataSettings.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.Entities.MetadataMatching; +namespace Kavita.Models.Entities.MetadataMatching; /// /// Handles the metadata settings for Kavita+ diff --git a/API/Entities/Person/ChapterPeople.cs b/Kavita.Models/Entities/Person/ChapterPeople.cs similarity index 88% rename from API/Entities/Person/ChapterPeople.cs rename to Kavita.Models/Entities/Person/ChapterPeople.cs index c6a08a7dd..e366b7816 100644 --- a/API/Entities/Person/ChapterPeople.cs +++ b/Kavita.Models/Entities/Person/ChapterPeople.cs @@ -1,6 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.Entities.Person; +namespace Kavita.Models.Entities.Person; public class ChapterPeople { diff --git a/API/Entities/Person/Person.cs b/Kavita.Models/Entities/Person/Person.cs similarity index 95% rename from API/Entities/Person/Person.cs rename to Kavita.Models/Entities/Person/Person.cs index ed57fd6d3..8a360d154 100644 --- a/API/Entities/Person/Person.cs +++ b/Kavita.Models/Entities/Person/Person.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Interfaces; -namespace API.Entities.Person; +namespace Kavita.Models.Entities.Person; public class Person : IHasCoverImage { diff --git a/API/Entities/Person/PersonAlias.cs b/Kavita.Models/Entities/Person/PersonAlias.cs similarity index 85% rename from API/Entities/Person/PersonAlias.cs rename to Kavita.Models/Entities/Person/PersonAlias.cs index f053f608d..d2f2027c1 100644 --- a/API/Entities/Person/PersonAlias.cs +++ b/Kavita.Models/Entities/Person/PersonAlias.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Person; +namespace Kavita.Models.Entities.Person; public class PersonAlias { diff --git a/API/Entities/Person/SeriesMetadataPeople.cs b/Kavita.Models/Entities/Person/SeriesMetadataPeople.cs similarity index 84% rename from API/Entities/Person/SeriesMetadataPeople.cs rename to Kavita.Models/Entities/Person/SeriesMetadataPeople.cs index caea10cd6..3de668e0e 100644 --- a/API/Entities/Person/SeriesMetadataPeople.cs +++ b/Kavita.Models/Entities/Person/SeriesMetadataPeople.cs @@ -1,7 +1,7 @@ -using API.Entities.Enums; -using API.Entities.Metadata; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; -namespace API.Entities.Person; +namespace Kavita.Models.Entities.Person; public class SeriesMetadataPeople { diff --git a/API/Entities/Progress/AppUserProgress.cs b/Kavita.Models/Entities/Progress/AppUserProgress.cs similarity index 94% rename from API/Entities/Progress/AppUserProgress.cs rename to Kavita.Models/Entities/Progress/AppUserProgress.cs index bf4283aa0..6e0458056 100644 --- a/API/Entities/Progress/AppUserProgress.cs +++ b/Kavita.Models/Entities/Progress/AppUserProgress.cs @@ -1,7 +1,8 @@ using System; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.User; -namespace API.Entities.Progress; +namespace Kavita.Models.Entities.Progress; /// /// Represents the progress a single user has on a given Chapter. diff --git a/API/Entities/Progress/AppUserReadingHistory.cs b/Kavita.Models/Entities/Progress/AppUserReadingHistory.cs similarity index 85% rename from API/Entities/Progress/AppUserReadingHistory.cs rename to Kavita.Models/Entities/Progress/AppUserReadingHistory.cs index 009687732..3190217db 100644 --- a/API/Entities/Progress/AppUserReadingHistory.cs +++ b/Kavita.Models/Entities/Progress/AppUserReadingHistory.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; -using API.DTOs.Progress; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; -namespace API.Entities.Progress; +namespace Kavita.Models.Entities.Progress; /// /// Represents a single day's worth of Reading Sessions diff --git a/API/Entities/Progress/AppUserReadingSession.cs b/Kavita.Models/Entities/Progress/AppUserReadingSession.cs similarity index 89% rename from API/Entities/Progress/AppUserReadingSession.cs rename to Kavita.Models/Entities/Progress/AppUserReadingSession.cs index be81d69d3..34a6b2f33 100644 --- a/API/Entities/Progress/AppUserReadingSession.cs +++ b/Kavita.Models/Entities/Progress/AppUserReadingSession.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; -using API.Entities.Interfaces; -using API.Services.Reading; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; -namespace API.Entities.Progress; +namespace Kavita.Models.Entities.Progress; /// /// Represents a reading session for a user. See diff --git a/API/Entities/Progress/AppUserReadingSessionActivityData.cs b/Kavita.Models/Entities/Progress/AppUserReadingSessionActivityData.cs similarity index 95% rename from API/Entities/Progress/AppUserReadingSessionActivityData.cs rename to Kavita.Models/Entities/Progress/AppUserReadingSessionActivityData.cs index fae6d1c69..3c5bdc387 100644 --- a/API/Entities/Progress/AppUserReadingSessionActivityData.cs +++ b/Kavita.Models/Entities/Progress/AppUserReadingSessionActivityData.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; -using API.DTOs.Progress; -using API.Entities.Enums; -using Microsoft.EntityFrameworkCore; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities.Enums; -namespace API.Entities.Progress; +namespace Kavita.Models.Entities.Progress; #nullable enable public class AppUserReadingSessionActivityData diff --git a/API/Entities/Progress/ClientInfoData.cs b/Kavita.Models/Entities/Progress/ClientInfoData.cs similarity index 96% rename from API/Entities/Progress/ClientInfoData.cs rename to Kavita.Models/Entities/Progress/ClientInfoData.cs index 3c11678fd..f25420f98 100644 --- a/API/Entities/Progress/ClientInfoData.cs +++ b/Kavita.Models/Entities/Progress/ClientInfoData.cs @@ -1,8 +1,7 @@ using System; -using API.Constants; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.Entities.Progress; +namespace Kavita.Models.Entities.Progress; #nullable enable public class ClientInfoData diff --git a/API/Entities/ReadingList.cs b/Kavita.Models/Entities/ReadingList.cs similarity index 93% rename from API/Entities/ReadingList.cs rename to Kavita.Models/Entities/ReadingList.cs index 4a11845af..c4916aa1d 100644 --- a/API/Entities/ReadingList.cs +++ b/Kavita.Models/Entities/ReadingList.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; -using API.Entities.Enums; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.User; -namespace API.Entities; +namespace Kavita.Models.Entities; #nullable enable diff --git a/API/Entities/ReadingListItem.cs b/Kavita.Models/Entities/ReadingListItem.cs similarity index 94% rename from API/Entities/ReadingListItem.cs rename to Kavita.Models/Entities/ReadingListItem.cs index c9d1de5db..6dfa1cc9d 100644 --- a/API/Entities/ReadingListItem.cs +++ b/Kavita.Models/Entities/ReadingListItem.cs @@ -1,4 +1,4 @@ -namespace API.Entities; +namespace Kavita.Models.Entities; public class ReadingListItem { diff --git a/API/Entities/Scrobble/ScrobbleError.cs b/Kavita.Models/Entities/Scrobble/ScrobbleError.cs similarity index 90% rename from API/Entities/Scrobble/ScrobbleError.cs rename to Kavita.Models/Entities/Scrobble/ScrobbleError.cs index 5db780bfc..32e416c51 100644 --- a/API/Entities/Scrobble/ScrobbleError.cs +++ b/Kavita.Models/Entities/Scrobble/ScrobbleError.cs @@ -1,7 +1,7 @@ using System; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Interfaces; -namespace API.Entities.Scrobble; +namespace Kavita.Models.Entities.Scrobble; /// /// When a series is not found, we report it here diff --git a/API/Entities/Scrobble/ScrobbleEvent.cs b/Kavita.Models/Entities/Scrobble/ScrobbleEvent.cs similarity index 93% rename from API/Entities/Scrobble/ScrobbleEvent.cs rename to Kavita.Models/Entities/Scrobble/ScrobbleEvent.cs index 89d2701bc..977631f7c 100644 --- a/API/Entities/Scrobble/ScrobbleEvent.cs +++ b/Kavita.Models/Entities/Scrobble/ScrobbleEvent.cs @@ -1,8 +1,9 @@ using System; -using API.DTOs.Scrobbling; -using API.Entities.Interfaces; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.User; -namespace API.Entities.Scrobble; +namespace Kavita.Models.Entities.Scrobble; #nullable enable /// diff --git a/API/Entities/Scrobble/ScrobbleEventFilter.cs b/Kavita.Models/Entities/Scrobble/ScrobbleEventFilter.cs similarity index 93% rename from API/Entities/Scrobble/ScrobbleEventFilter.cs rename to Kavita.Models/Entities/Scrobble/ScrobbleEventFilter.cs index 1153e90e9..a029c6c70 100644 --- a/API/Entities/Scrobble/ScrobbleEventFilter.cs +++ b/Kavita.Models/Entities/Scrobble/ScrobbleEventFilter.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Scrobble; +namespace Kavita.Models.Entities.Scrobble; public class ScrobbleEventFilter { diff --git a/API/Entities/Scrobble/ScrobbleEventSortField.cs b/Kavita.Models/Entities/Scrobble/ScrobbleEventSortField.cs similarity index 78% rename from API/Entities/Scrobble/ScrobbleEventSortField.cs rename to Kavita.Models/Entities/Scrobble/ScrobbleEventSortField.cs index 51b3a2146..fd65e47ae 100644 --- a/API/Entities/Scrobble/ScrobbleEventSortField.cs +++ b/Kavita.Models/Entities/Scrobble/ScrobbleEventSortField.cs @@ -1,4 +1,4 @@ -namespace API.Entities.Scrobble; +namespace Kavita.Models.Entities.Scrobble; public enum ScrobbleEventSortField { diff --git a/API/Entities/Scrobble/ScrobbleHold.cs b/Kavita.Models/Entities/Scrobble/ScrobbleHold.cs similarity index 78% rename from API/Entities/Scrobble/ScrobbleHold.cs rename to Kavita.Models/Entities/Scrobble/ScrobbleHold.cs index c6f3afdb1..073723a94 100644 --- a/API/Entities/Scrobble/ScrobbleHold.cs +++ b/Kavita.Models/Entities/Scrobble/ScrobbleHold.cs @@ -1,7 +1,8 @@ using System; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.User; -namespace API.Entities.Scrobble; +namespace Kavita.Models.Entities.Scrobble; public class ScrobbleHold : IEntityDate { diff --git a/API/Entities/Series.cs b/Kavita.Models/Entities/Series.cs similarity index 96% rename from API/Entities/Series.cs rename to Kavita.Models/Entities/Series.cs index 395236ae3..f9d4f760d 100644 --- a/API/Entities/Series.cs +++ b/Kavita.Models/Entities/Series.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; -using API.Entities.Enums; -using API.Entities.Interfaces; -using API.Entities.Metadata; -using API.Entities.Progress; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; -namespace API.Entities; +namespace Kavita.Models.Entities; public class Series : IEntityDate, IHasReadTimeEstimate, IHasCoverImage { diff --git a/API/Entities/ServerSetting.cs b/Kavita.Models/Entities/ServerSetting.cs similarity index 82% rename from API/Entities/ServerSetting.cs rename to Kavita.Models/Entities/ServerSetting.cs index 37e85efae..95bcd83e7 100644 --- a/API/Entities/ServerSetting.cs +++ b/Kavita.Models/Entities/ServerSetting.cs @@ -1,8 +1,8 @@ using System.ComponentModel.DataAnnotations; -using API.Entities.Enums; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; -namespace API.Entities; +namespace Kavita.Models.Entities; public class ServerSetting : IHasConcurrencyToken { diff --git a/API/Entities/ServerStatistics.cs b/Kavita.Models/Entities/ServerStatistics.cs similarity index 92% rename from API/Entities/ServerStatistics.cs rename to Kavita.Models/Entities/ServerStatistics.cs index 159b7ef4c..f4fa46d28 100644 --- a/API/Entities/ServerStatistics.cs +++ b/Kavita.Models/Entities/ServerStatistics.cs @@ -1,4 +1,4 @@ -namespace API.Entities; +namespace Kavita.Models.Entities; public class ServerStatistics { diff --git a/API/Entities/SideNavStreamType.cs b/Kavita.Models/Entities/SideNavStreamType.cs similarity index 85% rename from API/Entities/SideNavStreamType.cs rename to Kavita.Models/Entities/SideNavStreamType.cs index 62f429889..104a6c350 100644 --- a/API/Entities/SideNavStreamType.cs +++ b/Kavita.Models/Entities/SideNavStreamType.cs @@ -1,4 +1,4 @@ -namespace API.Entities; +namespace Kavita.Models.Entities; public enum SideNavStreamType { diff --git a/API/Entities/SiteTheme.cs b/Kavita.Models/Entities/SiteTheme.cs similarity index 81% rename from API/Entities/SiteTheme.cs rename to Kavita.Models/Entities/SiteTheme.cs index 107dca556..852732907 100644 --- a/API/Entities/SiteTheme.cs +++ b/Kavita.Models/Entities/SiteTheme.cs @@ -1,9 +1,9 @@ using System; -using API.Entities.Enums.Theme; -using API.Entities.Interfaces; -using API.Services; +using Kavita.Common.Extensions; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.Enums.Theme; -namespace API.Entities; +namespace Kavita.Models.Entities; /// /// Represents a set of css overrides the user can upload to Kavita and will load into webui /// @@ -63,4 +63,14 @@ public class SiteTheme : IEntityDate, ITheme public string CompatibleVersion { get; set; } #endregion + + public static readonly SiteTheme DefaultTheme = new() + { + Name = "Dark", + NormalizedName = "Dark".ToNormalized(), + Provider = ThemeProvider.System, + FileName = "dark.scss", + IsDefault = true, + Description = "Default theme shipped with Kavita" + }; } diff --git a/API/Entities/Tag.cs b/Kavita.Models/Entities/Tag.cs similarity index 85% rename from API/Entities/Tag.cs rename to Kavita.Models/Entities/Tag.cs index 277422713..d95356831 100644 --- a/API/Entities/Tag.cs +++ b/Kavita.Models/Entities/Tag.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; -using API.Entities.Metadata; +using Kavita.Models.Entities.Metadata; using Microsoft.EntityFrameworkCore; -namespace API.Entities; +namespace Kavita.Models.Entities; [Index(nameof(NormalizedTitle), IsUnique = true)] public class Tag diff --git a/API/Entities/User/AppRole.cs b/Kavita.Models/Entities/User/AppRole.cs similarity index 82% rename from API/Entities/User/AppRole.cs rename to Kavita.Models/Entities/User/AppRole.cs index ca46d1bb0..0d9ae0cc2 100644 --- a/API/Entities/User/AppRole.cs +++ b/Kavita.Models/Entities/User/AppRole.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Identity; -namespace API.Entities; +namespace Kavita.Models.Entities.User; public class AppRole : IdentityRole { diff --git a/API/Entities/User/AppUser.cs b/Kavita.Models/Entities/User/AppUser.cs similarity index 96% rename from API/Entities/User/AppUser.cs rename to Kavita.Models/Entities/User/AppUser.cs index d3d6eb3c4..cb59f1916 100644 --- a/API/Entities/User/AppUser.cs +++ b/Kavita.Models/Entities/User/AppUser.cs @@ -3,16 +3,14 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -using API.Entities.Enums; -using API.Entities.Interfaces; -using API.Entities.Progress; -using API.Entities.Scrobble; -using API.Entities.User; -using API.Helpers; +using Kavita.Common.Helpers; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.Scrobble; using Microsoft.AspNetCore.Identity; - -namespace API.Entities; +namespace Kavita.Models.Entities.User; public class AppUser : IdentityUser, IHasConcurrencyToken, IHasCoverImage { diff --git a/API/Entities/User/AppUserAnnotation.cs b/Kavita.Models/Entities/User/AppUserAnnotation.cs similarity index 96% rename from API/Entities/User/AppUserAnnotation.cs rename to Kavita.Models/Entities/User/AppUserAnnotation.cs index e6b3b33af..c26742e52 100644 --- a/API/Entities/User/AppUserAnnotation.cs +++ b/Kavita.Models/Entities/User/AppUserAnnotation.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Interfaces; -namespace API.Entities; +namespace Kavita.Models.Entities.User; /// /// Represents an annotation in the Epub reader diff --git a/API/Entities/User/AppUserAuthKey.cs b/Kavita.Models/Entities/User/AppUserAuthKey.cs similarity index 92% rename from API/Entities/User/AppUserAuthKey.cs rename to Kavita.Models/Entities/User/AppUserAuthKey.cs index 811a80e60..9537824cf 100644 --- a/API/Entities/User/AppUserAuthKey.cs +++ b/Kavita.Models/Entities/User/AppUserAuthKey.cs @@ -1,8 +1,8 @@ using System; -using API.Entities.Enums.User; +using Kavita.Models.Entities.Enums.User; using Microsoft.EntityFrameworkCore; -namespace API.Entities.User; +namespace Kavita.Models.Entities.User; [Index(nameof(Key), IsUnique = true)] [Index(nameof(ExpiresAtUtc), IsUnique = false)] diff --git a/API/Entities/User/AppUserBookmark.cs b/Kavita.Models/Entities/User/AppUserBookmark.cs similarity index 86% rename from API/Entities/User/AppUserBookmark.cs rename to Kavita.Models/Entities/User/AppUserBookmark.cs index c62f8685e..8bb53c236 100644 --- a/API/Entities/User/AppUserBookmark.cs +++ b/Kavita.Models/Entities/User/AppUserBookmark.cs @@ -1,9 +1,14 @@ using System; using System.Text.Json.Serialization; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Interfaces; -namespace API.Entities; +namespace Kavita.Models.Entities.User; +public class BookmarkSeriesPair +{ + public AppUserBookmark Bookmark { get; init; } = null!; + public Series Series { get; init; } = null!; +} /// /// Represents a saved page in a Chapter entity for a given user. diff --git a/API/Entities/User/AppUserChapterRating.cs b/Kavita.Models/Entities/User/AppUserChapterRating.cs similarity index 93% rename from API/Entities/User/AppUserChapterRating.cs rename to Kavita.Models/Entities/User/AppUserChapterRating.cs index 777862b49..1543a862f 100644 --- a/API/Entities/User/AppUserChapterRating.cs +++ b/Kavita.Models/Entities/User/AppUserChapterRating.cs @@ -1,7 +1,7 @@ using System; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Interfaces; -namespace API.Entities.User; +namespace Kavita.Models.Entities.User; #nullable enable public class AppUserChapterRating : IEntityDate diff --git a/API/Entities/User/AppUserCollection.cs b/Kavita.Models/Entities/User/AppUserCollection.cs similarity index 95% rename from API/Entities/User/AppUserCollection.cs rename to Kavita.Models/Entities/User/AppUserCollection.cs index 2a6d8faff..f15e47a64 100644 --- a/API/Entities/User/AppUserCollection.cs +++ b/Kavita.Models/Entities/User/AppUserCollection.cs @@ -1,11 +1,9 @@ using System; using System.Collections.Generic; -using API.Entities.Enums; -using API.Entities.Interfaces; -using API.Services.Plus; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; - -namespace API.Entities; +namespace Kavita.Models.Entities.User; /// /// Represents a Collection of Series for a given User diff --git a/API/Entities/User/AppUserDashboardStream.cs b/Kavita.Models/Entities/User/AppUserDashboardStream.cs similarity index 90% rename from API/Entities/User/AppUserDashboardStream.cs rename to Kavita.Models/Entities/User/AppUserDashboardStream.cs index a3554b277..113b064cf 100644 --- a/API/Entities/User/AppUserDashboardStream.cs +++ b/Kavita.Models/Entities/User/AppUserDashboardStream.cs @@ -1,7 +1,6 @@ -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; - -namespace API.Entities; +namespace Kavita.Models.Entities.User; public class AppUserDashboardStream { diff --git a/API/Entities/User/AppUserExternalSource.cs b/Kavita.Models/Entities/User/AppUserExternalSource.cs similarity index 87% rename from API/Entities/User/AppUserExternalSource.cs rename to Kavita.Models/Entities/User/AppUserExternalSource.cs index 502204831..7e1389f5f 100644 --- a/API/Entities/User/AppUserExternalSource.cs +++ b/Kavita.Models/Entities/User/AppUserExternalSource.cs @@ -1,4 +1,4 @@ -namespace API.Entities; +namespace Kavita.Models.Entities.User; public class AppUserExternalSource { diff --git a/API/Entities/User/AppUserOnDeckRemoval.cs b/Kavita.Models/Entities/User/AppUserOnDeckRemoval.cs similarity index 84% rename from API/Entities/User/AppUserOnDeckRemoval.cs rename to Kavita.Models/Entities/User/AppUserOnDeckRemoval.cs index 3b7b16f80..d627b462b 100644 --- a/API/Entities/User/AppUserOnDeckRemoval.cs +++ b/Kavita.Models/Entities/User/AppUserOnDeckRemoval.cs @@ -1,4 +1,4 @@ -namespace API.Entities; +namespace Kavita.Models.Entities.User; public class AppUserOnDeckRemoval { diff --git a/API/Entities/User/AppUserPreferences.cs b/Kavita.Models/Entities/User/AppUserPreferences.cs similarity index 96% rename from API/Entities/User/AppUserPreferences.cs rename to Kavita.Models/Entities/User/AppUserPreferences.cs index ab2d1b081..8091f4c7c 100644 --- a/API/Entities/User/AppUserPreferences.cs +++ b/Kavita.Models/Entities/User/AppUserPreferences.cs @@ -1,11 +1,9 @@ using System; using System.Collections.Generic; -using API.Data; -using API.Entities.Enums; -using API.Entities.Enums.UserPreferences; -using API.Services.Tasks; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.UserPreferences; -namespace API.Entities.User; +namespace Kavita.Models.Entities.User; public class AppUserPreferences { @@ -81,7 +79,7 @@ public class AppUserPreferences /// /// Book Reader Option: Maps to the default Kavita font-family (inherit) or an override /// - public string BookReaderFontFamily { get; set; } = FontService.DefaultFont; + public string BookReaderFontFamily { get; set; } = EpubFont.DefaultFont; /// /// Book Reader Option: Allows tapping on side of screens to paginate /// @@ -140,7 +138,7 @@ public class AppUserPreferences /// UI Site Global Setting: The UI theme the user should use. /// /// Should default to Dark - public required SiteTheme Theme { get; set; } = Seed.DefaultThemes[0]; + public required SiteTheme Theme { get; set; } = SiteTheme.DefaultTheme; /// /// Global Site Option: If the UI should layout items as Cards or List items /// diff --git a/API/Entities/User/AppUserRating.cs b/Kavita.Models/Entities/User/AppUserRating.cs similarity index 90% rename from API/Entities/User/AppUserRating.cs rename to Kavita.Models/Entities/User/AppUserRating.cs index d49f9a3fd..d7530c49e 100644 --- a/API/Entities/User/AppUserRating.cs +++ b/Kavita.Models/Entities/User/AppUserRating.cs @@ -1,8 +1,7 @@ - -using System; -using API.Entities.Interfaces; +using System; +using Kavita.Models.Entities.Interfaces; -namespace API.Entities; +namespace Kavita.Models.Entities.User; #nullable enable public class AppUserRating : IEntityDate diff --git a/API/Entities/User/AppUserReadingProfile.cs b/Kavita.Models/Entities/User/AppUserReadingProfile.cs similarity index 96% rename from API/Entities/User/AppUserReadingProfile.cs rename to Kavita.Models/Entities/User/AppUserReadingProfile.cs index 4a68db4d5..439b27e64 100644 --- a/API/Entities/User/AppUserReadingProfile.cs +++ b/Kavita.Models/Entities/User/AppUserReadingProfile.cs @@ -1,10 +1,9 @@ using System.Collections.Generic; using System.ComponentModel; -using API.Entities.Enums; -using API.Entities.Enums.UserPreferences; -using API.Services.Tasks; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.UserPreferences; -namespace API.Entities; +namespace Kavita.Models.Entities.User; public enum BreakPoint { @@ -111,7 +110,7 @@ public class AppUserReadingProfile /// /// Book Reader Option: Maps to the default Kavita font-family (inherit) or an override /// - public string BookReaderFontFamily { get; set; } = FontService.DefaultFont; + public string BookReaderFontFamily { get; set; } = EpubFont.DefaultFont; /// /// Book Reader Option: Allows tapping on side of screens to paginate /// diff --git a/API/Entities/User/AppUserRole.cs b/Kavita.Models/Entities/User/AppUserRole.cs similarity index 82% rename from API/Entities/User/AppUserRole.cs rename to Kavita.Models/Entities/User/AppUserRole.cs index 9ee798e6b..895162098 100644 --- a/API/Entities/User/AppUserRole.cs +++ b/Kavita.Models/Entities/User/AppUserRole.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Identity; -namespace API.Entities; +namespace Kavita.Models.Entities.User; public class AppUserRole : IdentityUserRole { diff --git a/API/Entities/User/AppUserSideNavStream.cs b/Kavita.Models/Entities/User/AppUserSideNavStream.cs similarity index 95% rename from API/Entities/User/AppUserSideNavStream.cs rename to Kavita.Models/Entities/User/AppUserSideNavStream.cs index a164b0a1f..963048e06 100644 --- a/API/Entities/User/AppUserSideNavStream.cs +++ b/Kavita.Models/Entities/User/AppUserSideNavStream.cs @@ -1,4 +1,4 @@ -namespace API.Entities; +namespace Kavita.Models.Entities.User; public class AppUserSideNavStream { diff --git a/API/Entities/User/AppUserSmartFilter.cs b/Kavita.Models/Entities/User/AppUserSmartFilter.cs similarity index 88% rename from API/Entities/User/AppUserSmartFilter.cs rename to Kavita.Models/Entities/User/AppUserSmartFilter.cs index e9f58fb5c..6933fa55d 100644 --- a/API/Entities/User/AppUserSmartFilter.cs +++ b/Kavita.Models/Entities/User/AppUserSmartFilter.cs @@ -1,6 +1,4 @@ -using API.DTOs.Filtering.v2; - -namespace API.Entities; +namespace Kavita.Models.Entities.User; /// /// Represents a Saved user Filter diff --git a/API/Entities/User/AppUserTableOfContent.cs b/Kavita.Models/Entities/User/AppUserTableOfContent.cs similarity index 95% rename from API/Entities/User/AppUserTableOfContent.cs rename to Kavita.Models/Entities/User/AppUserTableOfContent.cs index 5d110b8b6..18968b9dc 100644 --- a/API/Entities/User/AppUserTableOfContent.cs +++ b/Kavita.Models/Entities/User/AppUserTableOfContent.cs @@ -1,7 +1,7 @@ using System; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Interfaces; -namespace API.Entities; +namespace Kavita.Models.Entities.User; /// /// A personal table of contents for a given user linked with a given book diff --git a/API/Entities/User/AppUserWantToRead.cs b/Kavita.Models/Entities/User/AppUserWantToRead.cs similarity index 91% rename from API/Entities/User/AppUserWantToRead.cs rename to Kavita.Models/Entities/User/AppUserWantToRead.cs index d41e44962..f133d491c 100644 --- a/API/Entities/User/AppUserWantToRead.cs +++ b/Kavita.Models/Entities/User/AppUserWantToRead.cs @@ -1,4 +1,4 @@ -namespace API.Entities; +namespace Kavita.Models.Entities.User; public class AppUserWantToRead { diff --git a/API/Entities/User/ClientDevice.cs b/Kavita.Models/Entities/User/ClientDevice.cs similarity index 95% rename from API/Entities/User/ClientDevice.cs rename to Kavita.Models/Entities/User/ClientDevice.cs index ea8cdcc1c..fff09e687 100644 --- a/API/Entities/User/ClientDevice.cs +++ b/Kavita.Models/Entities/User/ClientDevice.cs @@ -1,9 +1,8 @@ using System; using System.Collections.Generic; -using API.Constants; -using API.Entities.Progress; +using Kavita.Models.Entities.Progress; -namespace API.Entities.User; +namespace Kavita.Models.Entities.User; #nullable enable public class ClientDevice diff --git a/API/Entities/User/ClientDeviceHistory.cs b/Kavita.Models/Entities/User/ClientDeviceHistory.cs similarity index 85% rename from API/Entities/User/ClientDeviceHistory.cs rename to Kavita.Models/Entities/User/ClientDeviceHistory.cs index 8981073a8..cd4c2c879 100644 --- a/API/Entities/User/ClientDeviceHistory.cs +++ b/Kavita.Models/Entities/User/ClientDeviceHistory.cs @@ -1,8 +1,7 @@ using System; -using API.Entities.Progress; -using API.Entities.User; +using Kavita.Models.Entities.Progress; -namespace API.Entities; +namespace Kavita.Models.Entities.User; public class ClientDeviceHistory { diff --git a/API/Entities/Volume.cs b/Kavita.Models/Entities/Volume.cs similarity index 97% rename from API/Entities/Volume.cs rename to Kavita.Models/Entities/Volume.cs index d5e9cf1c2..22faa2173 100644 --- a/API/Entities/Volume.cs +++ b/Kavita.Models/Entities/Volume.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Globalization; -using API.Entities.Interfaces; +using Kavita.Models.Entities.Interfaces; -namespace API.Entities; +namespace Kavita.Models.Entities; public class Volume : IEntityDate, IHasReadTimeEstimate, IHasCoverImage { diff --git a/Kavita.Models/Extensions/AppUserExtensions.cs b/Kavita.Models/Extensions/AppUserExtensions.cs new file mode 100644 index 000000000..25b28db36 --- /dev/null +++ b/Kavita.Models/Extensions/AppUserExtensions.cs @@ -0,0 +1,53 @@ +using System.Linq; +using Kavita.Models.Entities; +using Kavita.Models.Entities.User; +using Kavita.Models.Helpers; + +namespace Kavita.Models.Extensions; + +public static class AppUserExtensions +{ + /// + extension(AppUser user) + { + /// + /// Adds a new SideNavStream to the user's SideNavStreams. This user should have these streams already loaded + /// + /// + public void CreateSideNavFromLibrary(Library library) + { + var maxCount = user.SideNavStreams.Select(s => s.Order).DefaultIfEmpty().Max(); + + if (user.SideNavStreams.FirstOrDefault(s => s.LibraryId == library.Id) != null) return; + + user.SideNavStreams.Add(new AppUserSideNavStream + { + Name = library.Name, + Order = maxCount + 1, + IsProvided = false, + StreamType = SideNavStreamType.Library, + LibraryId = library.Id, + Visible = true, + }); + } + + public void RemoveSideNavFromLibrary(Library library) + { + // Find the library and remove it + var item = user.SideNavStreams.FirstOrDefault(s => s.LibraryId == library.Id); + if (item == null) return; + user.SideNavStreams.Remove(item); + + OrderableHelper.ReorderItems(user.SideNavStreams); + } + + public AgeRestriction GetAgeRestriction() + { + return new AgeRestriction + { + AgeRating = user.AgeRestriction, + IncludeUnknowns = user.AgeRestrictionIncludeUnknowns, + }; + } + } +} diff --git a/Kavita.Models/Extensions/ApplicationServiceExtensions.cs b/Kavita.Models/Extensions/ApplicationServiceExtensions.cs new file mode 100644 index 000000000..fe83e2e6b --- /dev/null +++ b/Kavita.Models/Extensions/ApplicationServiceExtensions.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Kavita.Models.Extensions; + +public static class ApplicationServiceExtensions +{ + public static void AddMappings(this IServiceCollection services) + { + services.AddAutoMapper(typeof(ApplicationServiceExtensions).Assembly); + } +} diff --git a/API/Extensions/EncodeFormatExtensions.cs b/Kavita.Models/Extensions/EncodeFormatExtensions.cs similarity index 82% rename from API/Extensions/EncodeFormatExtensions.cs rename to Kavita.Models/Extensions/EncodeFormatExtensions.cs index 924ae8b89..9413925b9 100644 --- a/API/Extensions/EncodeFormatExtensions.cs +++ b/Kavita.Models/Extensions/EncodeFormatExtensions.cs @@ -1,8 +1,7 @@ -using System; -using API.Entities.Enums; +using System; +using Kavita.Models.Entities.Enums; -namespace API.Extensions; -#nullable enable +namespace Kavita.Models.Extensions; public static class EncodeFormatExtensions { diff --git a/Kavita.Models/Extensions/EnumerableExtensions.cs b/Kavita.Models/Extensions/EnumerableExtensions.cs new file mode 100644 index 000000000..579198d57 --- /dev/null +++ b/Kavita.Models/Extensions/EnumerableExtensions.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Linq; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; + +namespace Kavita.Models.Extensions; + +public static class EnumerableExtensions +{ + public static IEnumerable RestrictAgainstAgeRestriction(this IEnumerable items, AgeRestriction restriction) + { + if (restriction.AgeRating == AgeRating.NotApplicable) return items; + var q = items.Where(s => s.AgeRating <= restriction.AgeRating); + if (!restriction.IncludeUnknowns) + { + return q.Where(s => s.AgeRating != AgeRating.Unknown); + } + + return q; + } + + public static IEnumerable RestrictAgainstAgeRestriction(this IEnumerable items, AgeRestriction restriction) + { + if (restriction.AgeRating == AgeRating.NotApplicable) return items; + var q = items.Where(s => s.AgeRating <= restriction.AgeRating); + if (!restriction.IncludeUnknowns) + { + return q.Where(s => s.AgeRating != AgeRating.Unknown); + } + + return q; + } + + public static IEnumerable RestrictAgainstAgeRestriction(this IEnumerable items, AgeRestriction restriction) + { + if (restriction.AgeRating == AgeRating.NotApplicable) return items; + var q = items.Where(s => s.AgeRating <= restriction.AgeRating); + if (!restriction.IncludeUnknowns) + { + return q.Where(s => s.AgeRating != AgeRating.Unknown); + } + + return q; + } +} diff --git a/API/Extensions/FilterDtoExtensions.cs b/Kavita.Models/Extensions/FilterDtoExtensions.cs similarity index 76% rename from API/Extensions/FilterDtoExtensions.cs rename to Kavita.Models/Extensions/FilterDtoExtensions.cs index 7a55f7db9..385b921a3 100644 --- a/API/Extensions/FilterDtoExtensions.cs +++ b/Kavita.Models/Extensions/FilterDtoExtensions.cs @@ -1,10 +1,9 @@ -using System; +using System; using System.Collections.Generic; -using API.DTOs.Filtering; -using API.Entities.Enums; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.Entities.Enums; -namespace API.Extensions; -#nullable enable +namespace Kavita.Models.Extensions; public static class FilterDtoExtensions { diff --git a/API/Extensions/PlusMediaFormatExtensions.cs b/Kavita.Models/Extensions/PlusMediaFormatExtensions.cs similarity index 95% rename from API/Extensions/PlusMediaFormatExtensions.cs rename to Kavita.Models/Extensions/PlusMediaFormatExtensions.cs index a88b9c2f9..e6534da6e 100644 --- a/API/Extensions/PlusMediaFormatExtensions.cs +++ b/Kavita.Models/Extensions/PlusMediaFormatExtensions.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; -using API.DTOs.Scrobbling; -using API.Entities.Enums; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.Entities.Enums; -namespace API.Extensions; +namespace Kavita.Models.Extensions; public static class PlusMediaFormatExtensions { diff --git a/API/Helpers/OrderableHelper.cs b/Kavita.Models/Helpers/OrderableHelper.cs similarity index 94% rename from API/Helpers/OrderableHelper.cs rename to Kavita.Models/Helpers/OrderableHelper.cs index d4ff89573..9561fd2dc 100644 --- a/API/Helpers/OrderableHelper.cs +++ b/Kavita.Models/Helpers/OrderableHelper.cs @@ -1,8 +1,9 @@ -using System; +using System; using System.Collections.Generic; -using API.Entities; +using Kavita.Models.Entities; +using Kavita.Models.Entities.User; -namespace API.Helpers; +namespace Kavita.Models.Helpers; #nullable enable public static class OrderableHelper diff --git a/Kavita.Models/Kavita.Models.csproj b/Kavita.Models/Kavita.Models.csproj new file mode 100644 index 000000000..c858a6446 --- /dev/null +++ b/Kavita.Models/Kavita.Models.csproj @@ -0,0 +1,21 @@ + + + + net10.0 + disable + disable + + + + + + + + + + + + + + + diff --git a/API/Data/Metadata/ComicInfo.cs b/Kavita.Models/Metadata/ComicInfo.cs similarity index 65% rename from API/Data/Metadata/ComicInfo.cs rename to Kavita.Models/Metadata/ComicInfo.cs index 5c65f368b..708e54e91 100644 --- a/API/Data/Metadata/ComicInfo.cs +++ b/Kavita.Models/Metadata/ComicInfo.cs @@ -2,14 +2,11 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using API.Entities; -using API.Entities.Enums; -using API.Helpers; -using API.Services; using Kavita.Common.Extensions; -using Nager.ArticleNumber; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; -namespace API.Data.Metadata; +namespace Kavita.Models.Metadata; #nullable enable /// @@ -135,23 +132,26 @@ public class ComicInfo public IList GetPeopleForRole(PersonRole role) => role switch { + PersonRole.Writer => SplitNames(Writer), + PersonRole.Penciller => SplitNames(Penciller), + PersonRole.Inker => SplitNames(Inker), + PersonRole.Colorist => SplitNames(Colorist), + PersonRole.Letterer => SplitNames(Letterer), + PersonRole.CoverArtist => SplitNames(CoverArtist), + PersonRole.Editor => SplitNames(Editor), + PersonRole.Publisher => SplitNames(Publisher), + PersonRole.Translator => SplitNames(Translator), + PersonRole.Imprint => SplitNames(Imprint), + PersonRole.Character => SplitNames(Characters), + PersonRole.Team => SplitNames(Teams), + PersonRole.Location => SplitNames(Locations), PersonRole.Other => [], - PersonRole.Writer => TagHelper.GetTagValues(Writer), - PersonRole.Penciller => TagHelper.GetTagValues(Penciller), - PersonRole.Inker => TagHelper.GetTagValues(Inker), - PersonRole.Colorist => TagHelper.GetTagValues(Colorist), - PersonRole.Letterer => TagHelper.GetTagValues(Letterer), - PersonRole.CoverArtist => TagHelper.GetTagValues(CoverArtist), - PersonRole.Editor => TagHelper.GetTagValues(Editor), - PersonRole.Publisher => TagHelper.GetTagValues(Publisher), - PersonRole.Character => TagHelper.GetTagValues(Characters), - PersonRole.Translator => TagHelper.GetTagValues(Translator), - PersonRole.Imprint => TagHelper.GetTagValues(Imprint), - PersonRole.Team => TagHelper.GetTagValues(Teams), - PersonRole.Location => TagHelper.GetTagValues(Locations), _ => throw new ArgumentOutOfRangeException(nameof(role), role, null) }; + private static string[] SplitNames(string value) => + value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + public static AgeRating ConvertAgeRatingToEnum(string value) { if (string.IsNullOrEmpty(value)) return Entities.Enums.AgeRating.Unknown; @@ -159,41 +159,6 @@ public class ComicInfo .SingleOrDefault(t => t.ToDescription().ToUpperInvariant().Equals(value.ToUpperInvariant()), Entities.Enums.AgeRating.Unknown); } - public static void CleanComicInfo(ComicInfo? info) - { - if (info == null) return; - - info.Series = info.Series.Trim(); - info.SeriesSort = info.SeriesSort.Trim(); - info.LocalizedSeries = info.LocalizedSeries.Trim(); - - info.Writer = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Writer); - info.Colorist = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Colorist); - info.Editor = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Editor); - info.Inker = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Inker); - info.Letterer = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Letterer); - info.Penciller = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Penciller); - info.Publisher = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Publisher); - info.Imprint = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Imprint); - info.Characters = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Characters); - info.Translator = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Translator); - info.CoverArtist = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.CoverArtist); - info.Teams = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Teams); - info.Locations = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Locations); - - // We need to convert GTIN to ISBN - info.Isbn = ParseGtin(info.GTIN); - - if (!string.IsNullOrEmpty(info.Number)) - { - info.Number = info.Number.Trim().Replace(",", "."); // Corrective measure for non English OSes - } - - if (!string.IsNullOrEmpty(info.Volume)) - { - info.Volume = info.Volume.Trim(); - } - } /// /// Uses both Volume and Number to make an educated guess as to what count refers to and it's highest number. @@ -221,34 +186,5 @@ public class ComicInfo return 0; } - /// - /// For a given GTIN, attempts to parse out an ISBN and set the Isbn property. - /// - /// - /// - public static string ParseGtin(string? gtin) - { - if (string.IsNullOrEmpty(gtin)) return string.Empty; - - - // This is likely a valid ISBN - if (gtin[0] == '0') - { - var offset = gtin[1] == '-' ? 0 : 1; - var potentialIsbn = gtin[offset..]; - if (ArticleNumberHelper.IsValidIsbn13(potentialIsbn)) - { - return potentialIsbn; - } - } - - if (ArticleNumberHelper.IsValidIsbn10(gtin) || ArticleNumberHelper.IsValidIsbn13(gtin)) - { - return gtin; - } - - return string.Empty; - } - } diff --git a/API/Data/Scanner/Chunk.cs b/Kavita.Models/Misc/Chunk.cs similarity index 93% rename from API/Data/Scanner/Chunk.cs rename to Kavita.Models/Misc/Chunk.cs index 78091200d..1639bd893 100644 --- a/API/Data/Scanner/Chunk.cs +++ b/Kavita.Models/Misc/Chunk.cs @@ -1,4 +1,4 @@ -namespace API.Data.Scanner; +namespace Kavita.Models.Misc; /// /// Represents a set of Entities which is broken up and iterated on diff --git a/Kavita.Models/Parser/ParseScannedFiles.cs b/Kavita.Models/Parser/ParseScannedFiles.cs new file mode 100644 index 000000000..070ed6d68 --- /dev/null +++ b/Kavita.Models/Parser/ParseScannedFiles.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using Kavita.Models.Entities.Enums; + +namespace Kavita.Models.Parser; + +public class ParsedSeries +{ + /// + /// Name of the Series + /// + public required string Name { get; init; } + /// + /// Normalized Name of the Series + /// + public required string NormalizedName { get; init; } + /// + /// Format of the Series + /// + public required MangaFormat Format { get; init; } + /// + /// Has this Series changed or not aka do we need to process it or not. + /// + public bool HasChanged { get; set; } +} + +public class ScanResult +{ + /// + /// A list of files in the Folder. Empty if HasChanged = false + /// + public IList Files { get; set; } + /// + /// A nested folder from Library Root (at any level) + /// + public string Folder { get; set; } + /// + /// The library root + /// + public string LibraryRoot { get; set; } + /// + /// Was the Folder scanned or not. If not modified since last scan, this will be false and Files empty + /// + public bool HasChanged { get; set; } + /// + /// Set in Stage 2: Parsed Info from the Files + /// + public IList ParserInfos { get; set; } +} + +/// +/// The final product of ParseScannedFiles. This has all the processed parserInfo and is ready for tracking/processing into entities +/// +public class ScannedSeriesResult +{ + /// + /// Was the Folder scanned or not. If not modified since last scan, this will be false and indicates that upstream should count this as skipped + /// + public bool HasChanged { get; set; } + /// + /// The Parsed Series information used for tracking + /// + public ParsedSeries ParsedSeries { get; set; } + /// + /// Parsed files + /// + public IList ParsedInfos { get; set; } +} + +public class SeriesModified +{ + public required string? FolderPath { get; set; } + public required string? LowestFolderPath { get; set; } + public required string SeriesName { get; set; } + public DateTime LastScanned { get; set; } + public MangaFormat Format { get; set; } + public IEnumerable LibraryRoots { get; set; } = ArraySegment.Empty; +} diff --git a/API/Services/Tasks/Scanner/Parser/ParserInfo.cs b/Kavita.Models/Parser/ParserInfo.cs similarity index 71% rename from API/Services/Tasks/Scanner/Parser/ParserInfo.cs rename to Kavita.Models/Parser/ParserInfo.cs index 524547d8b..759c2795f 100644 --- a/API/Services/Tasks/Scanner/Parser/ParserInfo.cs +++ b/Kavita.Models/Parser/ParserInfo.cs @@ -1,8 +1,8 @@ -using API.Data.Metadata; -using API.Entities.Enums; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; -namespace API.Services.Tasks.Scanner.Parser; -#nullable enable +namespace Kavita.Models.Parser; /// /// This represents all parsed information from a single file @@ -19,11 +19,11 @@ public class ParserInfo /// public required string Series { get; set; } = string.Empty; /// - /// This can be filled in from ComicInfo.xml/Epub during scanning. Will update the SortName field on + /// This can be filled in from ComicInfo.xml/Epub during scanning. Will update the SortName field on /// public string SeriesSort { get; set; } = string.Empty; /// - /// This can be filled in from ComicInfo.xml/Epub during scanning. Will update the LocalizedName field on + /// This can be filled in from ComicInfo.xml/Epub during scanning. Will update the LocalizedName field on /// public string LocalizedSeries { get; set; } = string.Empty; /// @@ -72,39 +72,16 @@ public class ParserInfo public string Title { get; set; } = string.Empty; /// - /// This can be filled in from ComicInfo.xml during scanning. Will update the SortOrder field on . + /// This can be filled in from ComicInfo.xml during scanning. Will update the SortOrder field on . /// Falls back to Parsed Chapter number /// public float IssueOrder { get; set; } - /// - /// If the ParserInfo has the IsSpecial tag or both volumes and chapters are default aka 0 - /// - /// - public bool IsSpecialInfo() - { - return IsSpecial || (Parser.IsLooseLeafVolume(Volumes) && Parser.IsDefaultChapter(Chapters)); - } - /// /// This will contain any EXTRA comicInfo information parsed from the epub or archive. If there is an archive with comicInfo.xml AND it contains /// series, volume information, that will override what we parsed. /// public ComicInfo? ComicInfo { get; set; } - /// - /// Merges non-empty/null properties from info2 into this entity. - /// - /// This does not merge ComicInfo as they should always be the same - /// - public void Merge(ParserInfo? info2) - { - if (info2 == null) return; - Chapters = Parser.IsDefaultChapter(Chapters) ? info2.Chapters: Chapters; - Volumes = Parser.IsLooseLeafVolume(Volumes) ? info2.Volumes : Volumes; - Edition = string.IsNullOrEmpty(Edition) ? info2.Edition : Edition; - Title = string.IsNullOrEmpty(Title) ? info2.Title : Title; - Series = string.IsNullOrEmpty(Series) ? info2.Series : Series; - IsSpecial = IsSpecial || info2.IsSpecial; - } + } diff --git a/API.Tests/Helpers/BrowserHelperTests.cs b/Kavita.Server.Tests/Helpers/BrowserHelperTests.cs similarity index 99% rename from API.Tests/Helpers/BrowserHelperTests.cs rename to Kavita.Server.Tests/Helpers/BrowserHelperTests.cs index e6a9568a2..68ee857ed 100644 --- a/API.Tests/Helpers/BrowserHelperTests.cs +++ b/Kavita.Server.Tests/Helpers/BrowserHelperTests.cs @@ -1,9 +1,7 @@ -using API.Constants; -using API.Entities.Enums; -using API.Helpers; -using Xunit; +using Kavita.Models.Entities.Enums; +using Kavita.Server.Helpers; -namespace API.Tests.Helpers; +namespace Kavita.Server.Tests.Helpers; public class BrowserHelperTests { diff --git a/Kavita.Server.Tests/Kavita.Server.Tests.csproj b/Kavita.Server.Tests/Kavita.Server.Tests.csproj new file mode 100644 index 000000000..967f6059e --- /dev/null +++ b/Kavita.Server.Tests/Kavita.Server.Tests.csproj @@ -0,0 +1,34 @@ + + + + net10.0 + enable + enable + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + \ No newline at end of file diff --git a/Kavita.Server.Tests/ManualMigrations/MigrateSmartFilterEncodingTests.cs b/Kavita.Server.Tests/ManualMigrations/MigrateSmartFilterEncodingTests.cs new file mode 100644 index 000000000..2ebc1b0da --- /dev/null +++ b/Kavita.Server.Tests/ManualMigrations/MigrateSmartFilterEncodingTests.cs @@ -0,0 +1,36 @@ +using Kavita.Server.ManualMigrations.v0._7._11; + +namespace Kavita.Server.Tests.ManualMigrations; + +public class MigrateSmartFilterEncodingTests +{ + + [Theory] + [InlineData("", false)] + [InlineData("name=DC%20-%20On%20Deck&stmts=comparison%3D1%26field%3D20%26value%3D0,comparison%3D9%26field%3D20%26value%3D100,comparison%3D0%26field%3D19%26value%3D274&sortOptions=sortField%3D1&isAscending=True&limitTo=0&combination=1", true)] + [InlineData("name=English%20In%20Progress&stmts=comparison%253D8%252Cfield%253D7%252Cvalue%253D4%25252C3,comparison%253D3%252Cfield%253D20%252Cvalue%253D100,comparison%253D8%252Cfield%253D3%252Cvalue%253Dja,comparison%253D1%252Cfield%253D20%252Cvalue%253D0&sortOptions=sortField%3D7,isAscending%3DFalse&limitTo=0&combination=1", true)] + [InlineData("name=Unread%20Isekai%20Light%20Novels&stmts=comparison%253D0%25C2%25A6field%253D20%25C2%25A6value%253D0%EF%BF%BDcomparison%253D5%25C2%25A6field%253D6%25C2%25A6value%253D230%EF%BF%BDcomparison%253D8%25C2%25A6field%253D7%25C2%25A6value%253D4%EF%BF%BDcomparison%253D0%25C2%25A6field%253D19%25C2%25A6value%253D14&sortOptions=sortField%3D5%C2%A6isAscending%3DFalse&limitTo=0&combination=1", false)] + [InlineData("name=Zero&stmts=comparison%3d7%26field%3d1%26value%3d0&sortOptions=sortField=2&isAscending=False&limitTo=0&combination=1", true)] + public void Test_ShouldMigrateFilter(string filter, bool expected) + { + Assert.Equal(expected, MigrateSmartFilterEncoding.ShouldMigrateFilter(filter)); + } + + [Theory] + [InlineData("name=DC%20-%20On%20Deck&stmts=comparison%3D1%26field%3D20%26value%3D0,comparison%3D9%26field%3D20%26value%3D100,comparison%3D0%26field%3D19%26value%3D274&sortOptions=sortField%3D1&isAscending=True&limitTo=0&combination=1")] + [InlineData("name=Manga%20-%20On%20Deck&stmts=comparison%253D1%252Cfield%253D20%252Cvalue%253D0,comparison%253D3%252Cfield%253D20%252Cvalue%253D100,comparison%253D0%252Cfield%253D19%252Cvalue%253D2&sortOptions=sortField%3D1,isAscending%3DTrue&limitTo=0&combination=1")] + [InlineData("name=English%20In%20Progress&stmts=comparison%253D8%252Cfield%253D7%252Cvalue%253D4%25252C3,comparison%253D3%252Cfield%253D20%252Cvalue%253D100,comparison%253D8%252Cfield%253D3%252Cvalue%253Dja,comparison%253D1%252Cfield%253D20%252Cvalue%253D0&sortOptions=sortField%3D7,isAscending%3DFalse&limitTo=0&combination=1")] + public void MigrationWorks(string filter) + { + try + { + var updatedFilter = MigrateSmartFilterEncoding.EncodeFix(filter); + Assert.NotNull(updatedFilter); + } + catch (Exception ex) + { + Assert.Fail("Exception thrown: " + ex.Message); + } + + } +} diff --git a/API.Tests/Middleware/ClientInfoMiddlewareTests.cs b/Kavita.Server.Tests/Middleware/ClientInfoMiddlewareTests.cs similarity index 98% rename from API.Tests/Middleware/ClientInfoMiddlewareTests.cs rename to Kavita.Server.Tests/Middleware/ClientInfoMiddlewareTests.cs index a002e9f87..508269f1f 100644 --- a/API.Tests/Middleware/ClientInfoMiddlewareTests.cs +++ b/Kavita.Server.Tests/Middleware/ClientInfoMiddlewareTests.cs @@ -1,18 +1,13 @@ -using System; -using System.Threading.Tasks; -using API.Constants; -using API.Entities.Enums; -using API.Entities.Progress; -using API.Middleware; -using API.Services.Reading; -using API.Services.Store; +using Kavita.API.Store; +using Kavita.Common.Constants; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; +using Kavita.Server.Middleware; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; -namespace API.Tests.Middleware; -#nullable enable +namespace Kavita.Server.Tests.Middleware; public class ClientInfoMiddlewareTests { diff --git a/API/Assets/anilist-no-image-placeholder.jpg b/Kavita.Server/Assets/anilist-no-image-placeholder.jpg similarity index 100% rename from API/Assets/anilist-no-image-placeholder.jpg rename to Kavita.Server/Assets/anilist-no-image-placeholder.jpg diff --git a/API/Middleware/Attribute/DisallowRoleAttribute.cs b/Kavita.Server/Attributes/DisallowRoleAttribute.cs similarity index 94% rename from API/Middleware/Attribute/DisallowRoleAttribute.cs rename to Kavita.Server/Attributes/DisallowRoleAttribute.cs index 7cc1a7be2..92cf3dd89 100644 --- a/API/Middleware/Attribute/DisallowRoleAttribute.cs +++ b/Kavita.Server/Attributes/DisallowRoleAttribute.cs @@ -1,14 +1,14 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Extensions; -using API.Services; +using Kavita.API.Services; +using Kavita.Common.Extensions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; -namespace API.Middleware; +namespace Kavita.Server.Attributes; /// /// An attribute to prevent users with certain roles to access resources, or do actions. diff --git a/Kavita.Server/Attributes/EntityAccessAttribute.cs b/Kavita.Server/Attributes/EntityAccessAttribute.cs new file mode 100644 index 000000000..f5f8bf70b --- /dev/null +++ b/Kavita.Server/Attributes/EntityAccessAttribute.cs @@ -0,0 +1,150 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Kavita.API.Database; +using Kavita.Common.Extensions; +using Kavita.Models.Constants; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; + +namespace Kavita.Server.Attributes; + +/// +/// An attribute restricting access to entities based on the user's access to the reading list. +/// Returns 404 Not Found on failure +/// +/// +/// +public class ReadingListAccessAttribute(bool failOnMissing = true, string readingListIdKey = "readingListId") + : AccessAttribute(readingListIdKey, failOnMissing, false) +{ + protected override Task CheckAccess(IUnitOfWork unitOfWork, int userId, int entityId, CancellationToken ct) + { + return unitOfWork.UserRepository.HasAccessToReadingList(userId, entityId, ct); + } +} + +/// +/// An attribute restricting access to entities based on the user's access to the person. +/// Returns 404 Not Found on failure +/// +/// +/// +public class PersonAccessAttribute(bool failOnMissing = true, string personIdKey = "personId") + : AccessAttribute(personIdKey, failOnMissing) +{ + protected override Task CheckAccess(IUnitOfWork unitOfWork, int userId, int entityId, CancellationToken ct) + { + return unitOfWork.UserRepository.HasAccessToPerson(userId, entityId, ct); + } +} + +/// +/// An attribute restricting access to entities based on the user's access to the library. +/// Returns 404 Not Found on failure +/// +/// +/// +public class LibraryAccessAttribute(bool failOnMissing = true, string libraryIdKey = "libraryId") + : AccessAttribute(libraryIdKey, failOnMissing) +{ + + protected override Task CheckAccess(IUnitOfWork unitOfWork, int userId, int entityId, CancellationToken ct) + { + return unitOfWork.UserRepository.HasAccessToLibrary(userId, entityId, ct); + } +} + +/// +/// An attribute restricting access to entities based on the user's access to the series. +/// Returns 404 Not Found on failure +/// +/// +/// +public class SeriesAccessAttribute(bool failOnMissing = true, string seriesIdKey = "seriesId") + : AccessAttribute(seriesIdKey, failOnMissing) +{ + + protected override Task CheckAccess(IUnitOfWork unitOfWork, int userId, int entityId, CancellationToken ct) + { + return unitOfWork.UserRepository.HasAccessToSeries(userId, entityId, ct); + } +} + +/// +/// An attribute restricting access to entities based on the user's access to the volume. +/// Returns 404 Not Found on failure +/// +/// +/// +public class VolumeAccessAttribute(bool failOnMissing = true, string volumeIdKey = "volumeId") + : AccessAttribute(volumeIdKey, failOnMissing) +{ + + protected override Task CheckAccess(IUnitOfWork unitOfWork, int userId, int entityId, CancellationToken ct) + { + return unitOfWork.UserRepository.HasAccessToVolume(userId, entityId, ct); + } +} + +/// +/// An attribute restricting access to entities based on the user's access to the chapter. +/// Returns 404 Not Found on failure +/// +/// +/// +public class ChapterAccessAttribute(bool failOnMissing = true, string chapterIdKey = "chapterId") + : AccessAttribute(chapterIdKey, failOnMissing) +{ + + protected override Task CheckAccess(IUnitOfWork unitOfWork, int userId, int entityId, CancellationToken ct) + { + return unitOfWork.UserRepository.HasAccessToChapter(userId, entityId, ct); + } +} + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] +public abstract class AccessAttribute(string idKey, bool failOnMissing = true, bool alwaysAllowAdmin = true) : Attribute, IAsyncAuthorizationFilter +{ + + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) + { + var user = context.HttpContext.User; + if (alwaysAllowAdmin && user.IsInRole(PolicyConstants.AdminRole)) return; + + var userId = user.GetUserId(); + + var entityId = ExtractId(context.HttpContext, idKey); + + if (entityId == null) + { + if (failOnMissing) + { + context.Result = new NotFoundResult(); + } + return; + } + + var unitOfWork = context.HttpContext.RequestServices.GetRequiredService(); + + var hasAccess = await CheckAccess(unitOfWork, userId, entityId.Value, context.HttpContext.RequestAborted); + if (!hasAccess) + { + context.Result = new NotFoundResult(); + } + } + + protected abstract Task CheckAccess(IUnitOfWork unitOfWork, int userId, int entityId, CancellationToken ct); + + private static int? ExtractId(HttpContext httpContext, string key) + { + if (httpContext.Request.RouteValues.TryGetValue(key, out var pathVal) && + int.TryParse(pathVal?.ToString(), out var pId)) return pId; + + if (int.TryParse(httpContext.Request.Query[key], out var qId)) return qId; + + return null; + } +} diff --git a/Kavita.Server/Attributes/KPlusAttribute.cs b/Kavita.Server/Attributes/KPlusAttribute.cs new file mode 100644 index 000000000..27ac84b19 --- /dev/null +++ b/Kavita.Server/Attributes/KPlusAttribute.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading.Tasks; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Store; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; + +namespace Kavita.Server.Attributes; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] +public class KPlusAttribute : Attribute, IAsyncAuthorizationFilter +{ + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) + { + var userContext = context.HttpContext.RequestServices.GetRequiredService(); + if (!userContext.IsAuthenticated) + { + context.Result = new UnauthorizedResult(); + return; + } + + var licenseService = context.HttpContext.RequestServices.GetRequiredService(); + + if (!await licenseService.HasActiveLicense(ct: context.HttpContext.RequestAborted)) + { + var localizationService = context.HttpContext.RequestServices.GetRequiredService(); + var message = localizationService.Translate(userContext.GetUserIdOrThrow(), "kavitaplus-restricted"); + + context.Result = new BadRequestObjectResult(new {Message = message}); + } + + } +} diff --git a/API/Middleware/ProfilePrivacyAttribute.cs b/Kavita.Server/Attributes/ProfilePrivacyAttribute.cs similarity index 93% rename from API/Middleware/ProfilePrivacyAttribute.cs rename to Kavita.Server/Attributes/ProfilePrivacyAttribute.cs index bf46ac11c..07a6e1a35 100644 --- a/API/Middleware/ProfilePrivacyAttribute.cs +++ b/Kavita.Server/Attributes/ProfilePrivacyAttribute.cs @@ -1,15 +1,15 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.Extensions; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.Common.Extensions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; -namespace API.Middleware; +namespace Kavita.Server.Attributes; /// /// An attribute to restrict endpoint usage to either the user itself (authenticated user == userId) or the diff --git a/API/Controllers/AccountController.cs b/Kavita.Server/Controllers/AccountController.cs similarity index 51% rename from API/Controllers/AccountController.cs rename to Kavita.Server/Controllers/AccountController.cs index b142d628e..8841890d5 100644 --- a/API/Controllers/AccountController.cs +++ b/Kavita.Server/Controllers/AccountController.cs @@ -3,29 +3,33 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Account; -using API.DTOs.Email; -using API.DTOs.Settings; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Enums.User; -using API.Entities.User; -using API.Errors; -using API.Extensions; -using API.Helpers; -using API.Helpers.Builders; -using API.Middleware; -using API.Services; -using API.Services.Caching; -using API.SignalR; using AutoMapper; using Hangfire; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Errors; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; using Kavita.Common; using Kavita.Common.EnvironmentInfo; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Models.Builders; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Account; +using Kavita.Models.DTOs.Email; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.User; +using Kavita.Models.Entities.User; +using Kavita.Models.Extensions; +using Kavita.Server.Attributes; +using Kavita.Server.Extensions; +using Kavita.Services; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; @@ -34,56 +38,25 @@ using Microsoft.AspNetCore.RateLimiting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; /// /// All Account matters /// -public class AccountController : BaseApiController +/// +public class AccountController(UserManager userManager, + SignInManager signInManager, + ITokenService tokenService, IUnitOfWork unitOfWork, + ILogger logger, + IMapper mapper, IAccountService accountService, + IEmailService emailService, IEventHub eventHub, + ILocalizationService localizationService, + IAuthenticationSchemeProvider authenticationSchemeProvider, + IAuthKeyService authKeyService) : BaseApiController { // Hardcoded to avoid localization multiple enumeration: https://github.com/Kareadita/Kavita/issues/2829 private const string BadCredentialsMessage = "Your credentials are not correct"; - private readonly UserManager _userManager; - private readonly SignInManager _signInManager; - private readonly ITokenService _tokenService; - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - private readonly IMapper _mapper; - private readonly IAccountService _accountService; - private readonly IEmailService _emailService; - private readonly IEventHub _eventHub; - private readonly ILocalizationService _localizationService; - private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider; - private readonly IAuthKeyCacheInvalidator _authKeyCacheInvalidator; - - /// - public AccountController(UserManager userManager, - SignInManager signInManager, - ITokenService tokenService, IUnitOfWork unitOfWork, - ILogger logger, - IMapper mapper, IAccountService accountService, - IEmailService emailService, IEventHub eventHub, - ILocalizationService localizationService, - IAuthenticationSchemeProvider authenticationSchemeProvider, - IAuthKeyCacheInvalidator authKeyCacheInvalidator) - { - _userManager = userManager; - _signInManager = signInManager; - _tokenService = tokenService; - _unitOfWork = unitOfWork; - _logger = logger; - _mapper = mapper; - _accountService = accountService; - _emailService = emailService; - _eventHub = eventHub; - _localizationService = localizationService; - _authenticationSchemeProvider = authenticationSchemeProvider; - _authKeyCacheInvalidator = authKeyCacheInvalidator; - } - /// /// Returns true if OIDC authentication cookies are present and the /// scheme has been registered @@ -94,7 +67,7 @@ public class AccountController : BaseApiController [HttpGet("oidc-authenticated")] public async Task> OidcAuthenticated() { - var oidcScheme = await _authenticationSchemeProvider.GetSchemeAsync(IdentityServiceExtensions.OpenIdConnect); + var oidcScheme = await authenticationSchemeProvider.GetSchemeAsync(IdentityServiceExtensions.OpenIdConnect); return Ok(oidcScheme != null && HttpContext.Request.Cookies.ContainsKey(OidcService.CookieName)); } @@ -107,11 +80,11 @@ public class AccountController : BaseApiController [HttpGet] public async Task> GetCurrentUserAsync() { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.UserPreferences | AppUserIncludes.SideNavStreams | AppUserIncludes.AuthKeys); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.UserPreferences | AppUserIncludes.SideNavStreams | AppUserIncludes.AuthKeys); if (user == null) throw new UnauthorizedAccessException(); - var roles = await _userManager.GetRolesAsync(user); - if (!roles.Contains(PolicyConstants.LoginRole) && !roles.Contains(PolicyConstants.AdminRole)) return Unauthorized(await _localizationService.Translate(user.Id, "disabled-account")); + var roles = await userManager.GetRolesAsync(user); + if (!roles.Contains(PolicyConstants.LoginRole) && !roles.Contains(PolicyConstants.AdminRole)) return Unauthorized(await localizationService.Translate(user.Id, "disabled-account")); return Ok(await ConstructUserDto(user, roles, false)); } @@ -125,43 +98,43 @@ public class AccountController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task UpdatePassword(ResetPasswordDto resetPasswordDto) { - var user = await _userManager.Users.SingleOrDefaultAsync(x => x.UserName == 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", Username!, resetPasswordDto.UserName.Sanitize()); + logger.LogInformation("{UserName} is changing {ResetUser}'s password", Username!, resetPasswordDto.UserName.Sanitize()); var isAdmin = User.IsInRole(PolicyConstants.AdminRole); if (resetPasswordDto.UserName == Username! && !(User.IsInRole(PolicyConstants.ChangePasswordRole) || isAdmin)) - return Unauthorized(await _localizationService.Translate(UserId, "permission-denied")); + return Unauthorized(await localizationService.Translate(UserId, "permission-denied")); if (resetPasswordDto.UserName != Username! && !isAdmin) - return Unauthorized(await _localizationService.Translate(UserId, "permission-denied")); + return Unauthorized(await localizationService.Translate(UserId, "permission-denied")); if (string.IsNullOrEmpty(resetPasswordDto.OldPassword) && !isAdmin) return BadRequest( new ApiException(400, - await _localizationService.Translate(UserId, "password-required"))); + await localizationService.Translate(UserId, "password-required"))); - var oidcConfig = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; + var oidcConfig = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; if (user.IdentityProvider == IdentityProvider.OpenIdConnect && oidcConfig is {Enabled: true, SyncUserSettings: true}) { - return BadRequest(await _localizationService.Translate(user.Id, "oidc-managed")); + return BadRequest(await localizationService.Translate(user.Id, "oidc-managed")); } // If you're an admin and the username isn't yours, you don't need to validate the password var isResettingOtherUser = (resetPasswordDto.UserName != Username! && isAdmin); - if (!isResettingOtherUser && !await _userManager.CheckPasswordAsync(user, resetPasswordDto.OldPassword)) + if (!isResettingOtherUser && !await userManager.CheckPasswordAsync(user, resetPasswordDto.OldPassword)) { - return BadRequest(await _localizationService.Translate(UserId, "invalid-password")); + return BadRequest(await localizationService.Translate(UserId, "invalid-password")); } - var errors = await _accountService.ChangeUserPassword(user, resetPasswordDto.Password); + var errors = await accountService.ChangeUserPassword(user, resetPasswordDto.Password); if (errors.Any()) { return BadRequest(errors); } - _logger.LogInformation("{User}'s Password has been reset", resetPasswordDto.UserName); + logger.LogInformation("{User}'s Password has been reset", user.UserName); return Ok(); } @@ -174,12 +147,12 @@ public class AccountController : BaseApiController [HttpPost("register")] public async Task> RegisterFirstUser(RegisterDto registerDto) { - var admins = await _userManager.GetUsersInRoleAsync("Admin"); - if (admins.Count > 0) return BadRequest(await _localizationService.Get("en", "denied")); + var admins = await userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole); + if (admins.Count > 0) return BadRequest(await localizationService.Get("en", "denied")); try { - var usernameValidation = await _accountService.ValidateUsername(registerDto.Username); + var usernameValidation = await accountService.ValidateUsername(registerDto.Username); if (usernameValidation.Any()) { return BadRequest(usernameValidation); @@ -192,43 +165,43 @@ public class AccountController : BaseApiController } var user = new AppUserBuilder(registerDto.Username, registerDto.Email, - await _unitOfWork.SiteThemeRepository.GetDefaultTheme()).Build(); + await unitOfWork.SiteThemeRepository.GetDefaultTheme()).Build(); - var result = await _userManager.CreateAsync(user, registerDto.Password); + var result = await userManager.CreateAsync(user, registerDto.Password); if (!result.Succeeded) return BadRequest(result.Errors); - await _accountService.SeedUser(user); + await accountService.SeedUser(user); - var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); - if (string.IsNullOrEmpty(token)) return BadRequest(await _localizationService.Get("en", "confirm-token-gen")); - if (!await ConfirmEmailToken(token, user)) return BadRequest(await _localizationService.Get("en", "validate-email", token)); + var token = await userManager.GenerateEmailConfirmationTokenAsync(user); + if (string.IsNullOrEmpty(token)) return BadRequest(await localizationService.Get("en", "confirm-token-gen")); + if (!await ConfirmEmailToken(token, user)) return BadRequest(await localizationService.Get("en", "validate-email", token)); - var roleResult = await _userManager.AddToRoleAsync(user, PolicyConstants.AdminRole); + var roleResult = await userManager.AddToRoleAsync(user, PolicyConstants.AdminRole); if (!roleResult.Succeeded) return BadRequest(result.Errors); - await _userManager.AddToRoleAsync(user, PolicyConstants.LoginRole); + await userManager.AddToRoleAsync(user, PolicyConstants.LoginRole); return new UserDto { Username = user.UserName, Email = user.Email, - Token = await _tokenService.CreateToken(user), - RefreshToken = await _tokenService.CreateRefreshToken(user), + Token = await tokenService.CreateToken(user), + RefreshToken = await tokenService.CreateRefreshToken(user), ApiKey = user.GetOpdsAuthKey(), - Preferences = _mapper.Map(user.UserPreferences), - KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value, + Preferences = mapper.Map(user.UserPreferences), + KavitaVersion = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value, }; } catch (Exception ex) { - _logger.LogError(ex, "Something went wrong when registering user"); + logger.LogError(ex, "Something went wrong when registering user"); // We need to manually delete the User as we've already committed - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(registerDto.Username); - _unitOfWork.UserRepository.Delete(user); - await _unitOfWork.CommitAsync(); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(registerDto.Username); + unitOfWork.UserRepository.Delete(user); + await unitOfWork.CommitAsync(); } - return BadRequest(await _localizationService.Get("en", "register-user")); + return BadRequest(await localizationService.Get("en", "register-user")); } @@ -244,60 +217,60 @@ public class AccountController : BaseApiController AppUser? user; if (!string.IsNullOrEmpty(loginDto.ApiKey)) { - user = await _unitOfWork.UserRepository.GetUserByAuthKey(loginDto.ApiKey); + user = await unitOfWork.UserRepository.GetUserByAuthKey(loginDto.ApiKey); } else { - user = await _userManager.Users + user = await userManager.Users .Include(u => u.UserPreferences) .Include(u => u.AuthKeys) .AsSplitQuery() .SingleOrDefaultAsync(x => x.NormalizedUserName == loginDto.Username.ToUpperInvariant()); } - _logger.LogInformation("{UserName} attempting to login from {IpAddress}", loginDto.Username, HttpContext.Connection.RemoteIpAddress?.ToString()); + logger.LogInformation("{UserName} attempting to login from {IpAddress}", loginDto.Username.Sanitize(), HttpContext.Connection.RemoteIpAddress?.ToString()); if (user == null) { - _logger.LogWarning("Attempted login by {UserName} failed due to unable to find account", loginDto.Username); + logger.LogWarning("Attempted login by {UserName} failed due to unable to find account", loginDto.Username.Sanitize()); return Unauthorized(BadCredentialsMessage); } - var roles = await _userManager.GetRolesAsync(user); - if (!roles.Contains(PolicyConstants.LoginRole)) return Unauthorized(await _localizationService.Translate(user.Id, "disabled-account")); + var roles = await userManager.GetRolesAsync(user); + if (!roles.Contains(PolicyConstants.LoginRole)) return Unauthorized(await localizationService.Translate(user.Id, "disabled-account")); - var oidcConfig = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; + var oidcConfig = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; // Setting only takes effect if OIDC is functional, and if we're not logging in via ApiKey var disablePasswordAuthentication = oidcConfig is {Enabled: true, DisablePasswordAuthentication: true} && string.IsNullOrEmpty(loginDto.ApiKey); - if (disablePasswordAuthentication && !roles.Contains(PolicyConstants.AdminRole)) return Unauthorized(await _localizationService.Translate(user.Id, "password-authentication-disabled")); + if (disablePasswordAuthentication && !roles.Contains(PolicyConstants.AdminRole)) return Unauthorized(await localizationService.Translate(user.Id, "password-authentication-disabled")); if (string.IsNullOrEmpty(loginDto.ApiKey)) { - var result = await _signInManager + var result = await signInManager .CheckPasswordSignInAsync(user, loginDto.Password, true); if (result.IsLockedOut) { - await _userManager.UpdateSecurityStampAsync(user); - var errorStr = await _localizationService.Translate(user.Id, "locked-out"); - _logger.LogWarning("{UserName} failed to log in at {Time}: {Issue}", user.UserName, user.LastActive, + await userManager.UpdateSecurityStampAsync(user); + var errorStr = await localizationService.Translate(user.Id, "locked-out"); + logger.LogWarning("{UserName} failed to log in at {Time}: {Issue}", user.UserName, user.LastActive, errorStr); return Unauthorized(errorStr); } if (!result.Succeeded) { - string errorStr = result.IsNotAllowed - ? await _localizationService.Translate(user.Id, "confirm-email") + var errorStr = result.IsNotAllowed + ? await localizationService.Translate(user.Id, "confirm-email") : BadCredentialsMessage; - _logger.LogWarning("{UserName} failed to log in at {Time}: {Issue}", user.UserName, user.LastActive, errorStr); + logger.LogWarning("{UserName} failed to log in at {Time}: {Issue}", user.UserName, user.LastActive, errorStr); return Unauthorized(errorStr); } } - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(); - _logger.LogInformation("{UserName} logged in at {Time}", user.UserName, user.LastActive); + logger.LogInformation("{UserName} logged in at {Time}", user.UserName, user.LastActive); return Ok(await ConstructUserDto(user, roles)); } @@ -305,23 +278,23 @@ public class AccountController : BaseApiController private async Task ConstructUserDto(AppUser user, IList roles, bool includeTokens = true) { // TODO: Clean this up to be streamlined - var dto = _mapper.Map(user); + var dto = mapper.Map(user); if (includeTokens) { - dto.Token = await _tokenService.CreateToken(user); - dto.RefreshToken = await _tokenService.CreateRefreshToken(user); + dto.Token = await tokenService.CreateToken(user); + dto.RefreshToken = await tokenService.CreateRefreshToken(user); } dto.Roles = roles; - dto.KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value; // Why are we getting this from the DB? + dto.KavitaVersion = BuildInfo.Version.ToString(); - var pref = await _unitOfWork.UserRepository.GetPreferencesAsync(user.UserName!); + var pref = await unitOfWork.UserRepository.GetPreferencesAsync(user.UserName!); if (pref == null) return dto; - pref.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme(); - dto.Preferences = _mapper.Map(pref); - dto.AuthKeys = _mapper.Map>(user.AuthKeys); + pref.Theme ??= await unitOfWork.SiteThemeRepository.GetDefaultTheme(); + dto.Preferences = mapper.Map(pref); + dto.AuthKeys = mapper.Map>(user.AuthKeys); return dto; } @@ -333,10 +306,10 @@ public class AccountController : BaseApiController [HttpGet("refresh-account")] public async Task> RefreshAccount() { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.UserPreferences | AppUserIncludes.AuthKeys); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.UserPreferences | AppUserIncludes.AuthKeys); if (user == null) return Unauthorized(); - var roles = await _userManager.GetRolesAsync(user); + var roles = await userManager.GetRolesAsync(user); return Ok(await ConstructUserDto(user, roles, !HttpContext.Request.Cookies.ContainsKey(OidcService.CookieName))); } @@ -351,10 +324,10 @@ public class AccountController : BaseApiController [HttpPost("refresh-token")] public async Task> RefreshToken([FromBody] TokenRequestDto tokenRequestDto) { - var token = await _tokenService.ValidateRefreshToken(tokenRequestDto); + var token = await tokenService.ValidateRefreshToken(tokenRequestDto); if (token == null) { - return Unauthorized(new { message = await _localizationService.Get("en", "invalid-token") }); + return Unauthorized(new { message = await localizationService.Get("en", "invalid-token") }); } return Ok(token); @@ -387,56 +360,56 @@ public class AccountController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> UpdateEmail(UpdateEmailDto? dto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); if (user == null || dto == null || string.IsNullOrEmpty(dto.Email) || string.IsNullOrEmpty(dto.Password)) - return BadRequest(await _localizationService.Translate(UserId, "invalid-payload")); + return BadRequest(await localizationService.Translate(UserId, "invalid-payload")); - var oidcConfig = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; + var oidcConfig = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; if (user.IdentityProvider == IdentityProvider.OpenIdConnect && oidcConfig is {Enabled: true, SyncUserSettings: true}) { - return BadRequest(await _localizationService.Translate(user.Id, "oidc-managed")); + return BadRequest(await localizationService.Translate(user.Id, "oidc-managed")); } // Validate this user's password - if (! await _userManager.CheckPasswordAsync(user, dto.Password)) + if (! await userManager.CheckPasswordAsync(user, dto.Password)) { - _logger.LogWarning("A user tried to change {UserName}'s email, but password didn't validate", user.UserName); - return BadRequest(await _localizationService.Translate(UserId, "permission-denied")); + logger.LogWarning("A user tried to change {UserName}'s email, but password didn't validate", user.UserName); + return BadRequest(await localizationService.Translate(UserId, "permission-denied")); } // Validate no other users exist with this email if (user.Email!.Equals(dto.Email)) - return BadRequest(await _localizationService.Translate(UserId, "nothing-to-do")); + return BadRequest(await localizationService.Translate(UserId, "nothing-to-do")); // Check if email is used by another user - var existingUserEmail = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); + var existingUserEmail = await unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); if (existingUserEmail != null) { - return BadRequest(await _localizationService.Translate(UserId, "share-multiple-emails")); + return BadRequest(await localizationService.Translate(UserId, "share-multiple-emails")); } // All validations complete, generate a new token and email it to the user at the new address. Confirm email link will update the email - var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); + var token = await userManager.GenerateEmailConfirmationTokenAsync(user); if (string.IsNullOrEmpty(token)) { - _logger.LogError("There was an issue generating a token for the email"); - return BadRequest(await _localizationService.Translate(UserId, "generate-token")); + logger.LogError("There was an issue generating a token for the email"); + return BadRequest(await localizationService.Translate(UserId, "generate-token")); } - var isValidEmailAddress = _emailService.IsValidEmail(user.Email); - var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var isValidEmailAddress = emailService.IsValidEmail(user.Email); + var serverSettings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); var shouldEmailUser = serverSettings.IsEmailSetup() || !isValidEmailAddress; user.EmailConfirmed = !shouldEmailUser; user.ConfirmationToken = token; - await _userManager.UpdateAsync(user); + await userManager.UpdateAsync(user); - 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); + 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 (!shouldEmailUser) { - _logger.LogInformation("Cannot email admin, email not setup or admin email invalid"); + logger.LogInformation("Cannot email admin, email not setup or admin email invalid"); return Ok(new InviteUserResponse { EmailLink = string.Empty, @@ -451,7 +424,7 @@ public class AccountController : BaseApiController { if (!isValidEmailAddress) { - _logger.LogCritical("[Update Email]: User is trying to update their email, but their existing email ({Email}) isn't valid. No email will be send", user.Email); + logger.LogCritical("[Update Email]: User is trying to update their email, but their existing email ({Email}) isn't valid. No email will be send", user.Email); return Ok(new InviteUserResponse { EmailLink = string.Empty, @@ -463,9 +436,9 @@ public class AccountController : BaseApiController try { - var invitingUser = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).First().UserName!; + var invitingUser = (await unitOfWork.UserRepository.GetAdminUsersAsync()).First().UserName!; // Email the old address of the update change - BackgroundJob.Enqueue(() => _emailService.SendEmailChangeEmail(new ConfirmationEmailDto() + BackgroundJob.Enqueue(() => emailService.SendEmailChangeEmail(new ConfirmationEmailDto() { EmailAddress = string.IsNullOrEmpty(user.Email) ? dto.Email : user.Email, InstallId = BuildInfo.Version.ToString(), @@ -487,10 +460,10 @@ public class AccountController : BaseApiController } catch (Exception ex) { - _logger.LogError(ex, "There was an error during invite user flow, unable to send an email"); + logger.LogError(ex, "There was an error during invite user flow, unable to send an email"); } - await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id); + await eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id); return Ok(); } @@ -504,36 +477,36 @@ public class AccountController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task UpdateAgeRestriction(UpdateAgeRestrictionDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); - if (user == null) return Unauthorized(await _localizationService.Translate(UserId, "permission-denied")); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); + if (user == null) return Unauthorized(await localizationService.Translate(UserId, "permission-denied")); - var oidcConfig = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; + var oidcConfig = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; if (user.IdentityProvider == IdentityProvider.OpenIdConnect && oidcConfig is {Enabled: true, SyncUserSettings: true}) { - return BadRequest(await _localizationService.Translate(user.Id, "oidc-managed")); + return BadRequest(await localizationService.Translate(user.Id, "oidc-managed")); } - var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user); - var hasRole = await _accountService.CanChangeAgeRestriction(user); - if (!hasRole) return BadRequest(await _localizationService.Translate(UserId, "permission-denied")); + var isAdmin = await unitOfWork.UserRepository.IsUserAdminAsync(user); + var hasRole = await accountService.CanChangeAgeRestriction(user); + if (!hasRole) return BadRequest(await localizationService.Translate(UserId, "permission-denied")); user.AgeRestriction = isAdmin ? AgeRating.NotApplicable : dto.AgeRating; user.AgeRestrictionIncludeUnknowns = isAdmin || dto.IncludeUnknowns; - _unitOfWork.UserRepository.Update(user); + unitOfWork.UserRepository.Update(user); - if (!_unitOfWork.HasChanges()) return Ok(); + if (!unitOfWork.HasChanges()) return Ok(); try { - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); } catch (Exception ex) { - _logger.LogError(ex, "There was an error updating the age restriction"); - return BadRequest(await _localizationService.Translate(UserId, "age-restriction-update")); + logger.LogError(ex, "There was an error updating the age restriction"); + return BadRequest(await localizationService.Translate(UserId, "age-restriction-update")); } - await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id); + await eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id); return Ok(); } @@ -549,16 +522,16 @@ public class AccountController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task UpdateAccount(UpdateUserDto dto) { - var adminUser = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); + var adminUser = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); if (adminUser == null) return Unauthorized(); - if (!await _unitOfWork.UserRepository.IsUserAdminAsync(adminUser)) return Unauthorized(await _localizationService.Translate(UserId, "permission-denied")); + if (!await unitOfWork.UserRepository.IsUserAdminAsync(adminUser)) return Unauthorized(await localizationService.Translate(UserId, "permission-denied")); - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(dto.UserId, AppUserIncludes.SideNavStreams); - if (user == null) return BadRequest(await _localizationService.Translate(UserId, "no-user")); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(dto.UserId, AppUserIncludes.SideNavStreams); + if (user == null) return BadRequest(await localizationService.Translate(UserId, "no-user")); try { - if (await _accountService.ChangeIdentityProvider(UserId, user, dto.IdentityProvider)) return Ok(); + if (await accountService.ChangeIdentityProvider(UserId, user, dto.IdentityProvider)) return Ok(); } catch (KavitaException exception) { @@ -569,11 +542,11 @@ public class AccountController : BaseApiController if (!user.UserName!.Equals(dto.Username)) { // Validate username change - var errors = await _accountService.ValidateUsername(dto.Username); - if (errors.Any()) return BadRequest(await _localizationService.Translate(UserId, "username-taken")); + var errors = await accountService.ValidateUsername(dto.Username); + if (errors.Any()) return BadRequest(await localizationService.Translate(UserId, "username-taken")); user.UserName = dto.Username; - await _userManager.UpdateNormalizedUserNameAsync(user); - _unitOfWork.UserRepository.Update(user); + await userManager.UpdateNormalizedUserNameAsync(user); + unitOfWork.UserRepository.Update(user); } // Check if email is changing for a non-admin user @@ -581,18 +554,18 @@ public class AccountController : BaseApiController if (isUpdatingAnotherAccount && !string.IsNullOrEmpty(dto.Email) && user.Email != dto.Email) { // Validate username change - var errors = await _accountService.ValidateEmail(dto.Email); - if (errors.Any()) return BadRequest(await _localizationService.Translate(UserId, "email-taken")); + var errors = await accountService.ValidateEmail(dto.Email); + if (errors.Any()) return BadRequest(await localizationService.Translate(UserId, "email-taken")); user.Email = dto.Email; user.EmailConfirmed = true; // When an admin performs the flow, we assume the email address is able to receive data - await _userManager.UpdateNormalizedEmailAsync(user); - _unitOfWork.UserRepository.Update(user); + await userManager.UpdateNormalizedEmailAsync(user); + unitOfWork.UserRepository.Update(user); } // Update roles - var existingRoles = await _userManager.GetRolesAsync(user); + var existingRoles = await userManager.GetRolesAsync(user); var hasAdminRole = dto.Roles.Contains(PolicyConstants.AdminRole); if (!hasAdminRole) { @@ -603,9 +576,9 @@ public class AccountController : BaseApiController { var roles = dto.Roles; - var roleResult = await _userManager.RemoveFromRolesAsync(user, existingRoles); + var roleResult = await userManager.RemoveFromRolesAsync(user, existingRoles); if (!roleResult.Succeeded) return BadRequest(roleResult.Errors); - roleResult = await _userManager.AddToRolesAsync(user, roles); + roleResult = await userManager.AddToRolesAsync(user, roles); if (!roleResult.Succeeded) return BadRequest(roleResult.Errors); } @@ -613,12 +586,12 @@ public class AccountController : BaseApiController // await _userManager.UpdateSecurityStampAsync(user); to force them to re-authenticate - var allLibraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList(); + var allLibraries = (await unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList(); List libraries; if (hasAdminRole) { - _logger.LogInformation("{UserName} is being registered as admin. Granting access to all libraries", - user.UserName); + logger.LogInformation("{UserId} is being registered as admin. Granting access to all libraries", + user.Id); libraries = allLibraries; } else @@ -631,7 +604,7 @@ public class AccountController : BaseApiController user.RemoveSideNavFromLibrary(lib); } - libraries = (await _unitOfWork.LibraryRepository.GetLibraryForIdsAsync(dto.Libraries, LibraryIncludes.AppUser)).ToList(); + libraries = (await unitOfWork.LibraryRepository.GetLibraryForIdsAsync(dto.Libraries, LibraryIncludes.AppUser)).ToList(); } foreach (var lib in libraries) @@ -644,19 +617,19 @@ public class AccountController : BaseApiController user.AgeRestriction = hasAdminRole ? AgeRating.NotApplicable : dto.AgeRestriction.AgeRating; user.AgeRestrictionIncludeUnknowns = hasAdminRole || dto.AgeRestriction.IncludeUnknowns; - _unitOfWork.UserRepository.Update(user); + unitOfWork.UserRepository.Update(user); - if (!_unitOfWork.HasChanges() || await _unitOfWork.CommitAsync()) + if (!unitOfWork.HasChanges() || await unitOfWork.CommitAsync()) { - await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName), user.Id); - await _eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(user.Id), user.Id); + await eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName), user.Id); + await eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(user.Id), user.Id); // If we adjust library access, dashboards should re-render - await _eventHub.SendMessageToAsync(MessageFactory.DashboardUpdate, MessageFactory.DashboardUpdateEvent(user.Id), user.Id); + await eventHub.SendMessageToAsync(MessageFactory.DashboardUpdate, MessageFactory.DashboardUpdateEvent(user.Id), user.Id); return Ok(); } - await _unitOfWork.RollbackAsync(); - return BadRequest(await _localizationService.Translate(UserId, "generic-user-update")); + await unitOfWork.RollbackAsync(); + return BadRequest(await localizationService.Translate(UserId, "generic-user-update")); } /// @@ -669,14 +642,14 @@ public class AccountController : BaseApiController [HttpGet("invite-url")] public async Task> GetInviteUrl(int userId, bool withBaseUrl) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId); if (user == null) return Unauthorized(); if (user.EmailConfirmed) - return BadRequest(await _localizationService.Translate(UserId, "user-already-confirmed")); + return BadRequest(await localizationService.Translate(UserId, "user-already-confirmed")); if (string.IsNullOrEmpty(user.ConfirmationToken)) - return BadRequest(await _localizationService.Translate(UserId, "manual-setup-fail")); + return BadRequest(await localizationService.Translate(UserId, "manual-setup-fail")); - return await _emailService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", user.Email!, withBaseUrl); + return await emailService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", user.Email!, withBaseUrl); } @@ -690,34 +663,35 @@ public class AccountController : BaseApiController public async Task> InviteUser(InviteUserDto dto) { var userId = UserId; - var adminUser = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - if (adminUser == null) return Unauthorized(await _localizationService.Translate(userId, "permission-denied")); + var adminUser = await unitOfWork.UserRepository.GetUserByIdAsync(userId); + if (adminUser == null) return Unauthorized(await localizationService.Translate(userId, "permission-denied")); dto.Email = dto.Email.Trim(); - if (string.IsNullOrEmpty(dto.Email)) return BadRequest(await _localizationService.Translate(userId, "invalid-payload")); + if (string.IsNullOrEmpty(dto.Email)) return BadRequest(await localizationService.Translate(userId, "invalid-payload")); - _logger.LogInformation("{User} is inviting {Email} to the server", adminUser.UserName, dto.Email); + logger.LogInformation("{User} is inviting {Email} to the server", adminUser.UserName, dto.Email); // Check if there is an existing invite - var emailValidationErrors = await _accountService.ValidateEmail(dto.Email); + 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.Translate(UserId, "user-already-registered", invitedUser!.UserName)); - return BadRequest(await _localizationService.Translate(UserId, "user-already-invited")); + var invitedUser = await unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); + if (await userManager.IsEmailConfirmedAsync(invitedUser!)) + return BadRequest(await localizationService.Translate(UserId, "user-already-registered", invitedUser!.UserName)); + return BadRequest(await localizationService.Translate(UserId, "user-already-invited")); } // Create a new user var user = new AppUserBuilder(dto.Email, dto.Email, - await _unitOfWork.SiteThemeRepository.GetDefaultTheme()).Build(); - _unitOfWork.UserRepository.Add(user); + await unitOfWork.SiteThemeRepository.GetDefaultTheme()).Build(); + unitOfWork.UserRepository.Add(user); + try { - var result = await _userManager.CreateAsync(user, AccountService.DefaultPassword); + var result = await userManager.CreateAsync(user, AccountService.DefaultPassword); if (!result.Succeeded) return BadRequest(result.Errors); - await _accountService.SeedUser(user); + await accountService.SeedUser(user); // Assign Roles var roles = dto.Roles; @@ -734,7 +708,7 @@ public class AccountController : BaseApiController foreach (var role in roles) { if (!PolicyConstants.ValidRoles.Contains(role)) continue; - var roleResult = await _userManager.AddToRoleAsync(user, role); + var roleResult = await userManager.AddToRoleAsync(user, role); if (!roleResult.Succeeded) return BadRequest(roleResult.Errors); @@ -744,13 +718,13 @@ public class AccountController : BaseApiController List libraries; if (hasAdminRole) { - _logger.LogInformation("{UserName} is being registered as admin. Granting access to all libraries", - user.UserName); - libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync(LibraryIncludes.AppUser)).ToList(); + logger.LogInformation("{UserName} is being registered as admin. Granting access to all libraries", + user.UserName?.Sanitize()); + libraries = (await unitOfWork.LibraryRepository.GetLibrariesAsync(LibraryIncludes.AppUser)).ToList(); } else { - libraries = (await _unitOfWork.LibraryRepository.GetLibraryForIdsAsync(dto.Libraries, LibraryIncludes.AppUser)).ToList(); + libraries = (await unitOfWork.LibraryRepository.GetLibraryForIdsAsync(dto.Libraries, LibraryIncludes.AppUser)).ToList(); } foreach (var lib in libraries) @@ -763,34 +737,34 @@ public class AccountController : BaseApiController user.AgeRestriction = hasAdminRole ? AgeRating.NotApplicable : dto.AgeRestriction.AgeRating; user.AgeRestrictionIncludeUnknowns = hasAdminRole || dto.AgeRestriction.IncludeUnknowns; - var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); + var token = await userManager.GenerateEmailConfirmationTokenAsync(user); if (string.IsNullOrEmpty(token)) { - _logger.LogError("There was an issue generating a token for the email"); - return BadRequest(await _localizationService.Translate(UserId, "generic-invite-user")); + logger.LogError("There was an issue generating a token for the email"); + return BadRequest(await localizationService.Translate(UserId, "generic-invite-user")); } user.ConfirmationToken = token; - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(); } catch (Exception ex) { - _logger.LogError(ex, "There was an error during invite user flow, unable to create user. Deleting user for retry"); - _unitOfWork.UserRepository.Delete(user); - await _unitOfWork.CommitAsync(); - return BadRequest(await _localizationService.Translate(UserId, "generic-invite-user")); + logger.LogError(ex, "There was an error during invite user flow, unable to create user. Deleting user for retry"); + unitOfWork.UserRepository.Delete(user); + await unitOfWork.CommitAsync(); + return BadRequest(await localizationService.Translate(UserId, "generic-invite-user")); } try { - var emailLink = await _emailService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", dto.Email); - _logger.LogCritical("[Invite User]: Email Link for {UserName}: {Link}", user.UserName, emailLink); + var emailLink = await emailService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", dto.Email); + logger.LogCritical("[Invite User]: Email Link for {UserName}: {Link}", user.UserName?.Sanitize(), emailLink); - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - if (!_emailService.IsValidEmail(dto.Email) || !settings.IsEmailSetup()) + 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 or email is not setup", 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, @@ -799,7 +773,7 @@ public class AccountController : BaseApiController }); } - BackgroundJob.Enqueue(() => _emailService.SendInviteEmail(new ConfirmationEmailDto() + BackgroundJob.Enqueue(() => emailService.SendInviteEmail(new ConfirmationEmailDto() { EmailAddress = dto.Email, InvitingUser = adminUser.UserName, @@ -814,10 +788,10 @@ public class AccountController : BaseApiController } catch (Exception ex) { - _logger.LogError(ex, "There was an error during invite user flow, unable to send an email"); + logger.LogError(ex, "There was an error during invite user flow, unable to send an email"); } - return BadRequest(await _localizationService.Translate(UserId, "generic-invite-user")); + return BadRequest(await localizationService.Translate(UserId, "generic-invite-user")); } /// @@ -829,12 +803,12 @@ public class AccountController : BaseApiController [HttpPost("confirm-email")] public async Task> ConfirmEmail(ConfirmEmailDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); + var user = await unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); if (user == null) { - _logger.LogInformation("confirm-email failed from invalid registered email: {Email}", dto.Email); - return BadRequest(await _localizationService.Get("en", "invalid-email-confirmation")); + logger.LogInformation("confirm-email failed from invalid registered email: {Email}", dto.Email); + return BadRequest(await localizationService.Get("en", "invalid-email-confirmation")); } // Validate Password and Username @@ -842,10 +816,10 @@ public class AccountController : BaseApiController // This allows users that use a fake email with the same username to continue setting up the account if (!dto.Username.Equals(dto.Email) && !user.UserName!.Equals(dto.Username)) { - validationErrors.AddRange(await _accountService.ValidateUsername(dto.Username)); + validationErrors.AddRange(await accountService.ValidateUsername(dto.Username)); } - validationErrors.AddRange(await _accountService.ValidatePassword(user, dto.Password)); + validationErrors.AddRange(await accountService.ValidatePassword(user, dto.Password)); if (validationErrors.Any()) { @@ -855,21 +829,21 @@ public class AccountController : BaseApiController if (!await ConfirmEmailToken(dto.Token, user)) { - _logger.LogInformation("confirm-email failed from invalid token: {Token}", dto.Token); - return BadRequest(await _localizationService.Translate(user.Id, "invalid-email-confirmation")); + logger.LogInformation("confirm-email failed from invalid token: {Token}", dto.Token.Sanitize()); + return BadRequest(await localizationService.Translate(user.Id, "invalid-email-confirmation")); } user.UserName = dto.Username; user.ConfirmationToken = null; - var errors = await _accountService.ChangeUserPassword(user, dto.Password); + var errors = await accountService.ChangeUserPassword(user, dto.Password); if (errors.Any()) { return BadRequest(errors); } - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); - user = (await _unitOfWork.UserRepository.GetUserByUsernameAsync(user.UserName, + user = (await unitOfWork.UserRepository.GetUserByUsernameAsync(user.UserName, AppUserIncludes.UserPreferences | AppUserIncludes.AuthKeys))!; // Perform Login code @@ -877,11 +851,11 @@ public class AccountController : BaseApiController { Username = user.UserName!, Email = user.Email!, - Token = await _tokenService.CreateToken(user), - RefreshToken = await _tokenService.CreateRefreshToken(user), + Token = await tokenService.CreateToken(user), + RefreshToken = await tokenService.CreateRefreshToken(user), ApiKey = user.GetOpdsAuthKey(), - Preferences = _mapper.Map(user.UserPreferences), - KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value, + Preferences = mapper.Map(user.UserPreferences), + KavitaVersion = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value, }; } @@ -895,33 +869,33 @@ public class AccountController : BaseApiController [HttpPost("confirm-email-update")] public async Task ConfirmEmailUpdate(ConfirmEmailUpdateDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByConfirmationToken(dto.Token); + var user = await unitOfWork.UserRepository.GetUserByConfirmationToken(dto.Token); if (user == null) { - _logger.LogInformation("confirm-email failed from invalid registered email: {Email}", dto.Email); - return BadRequest(await _localizationService.Get("en", "invalid-email-confirmation")); + logger.LogInformation("confirm-email failed from invalid registered email: {Email}", dto.Email.Sanitize()); + return BadRequest(await localizationService.Get("en", "invalid-email-confirmation")); } if (!await ConfirmEmailToken(dto.Token, user)) { - _logger.LogInformation("confirm-email failed from invalid token: {Token}", dto.Token); - return BadRequest(await _localizationService.Translate(user.Id, "invalid-email-confirmation")); + logger.LogInformation("confirm-email failed from invalid token: {Token}", dto.Token.Sanitize()); + return BadRequest(await localizationService.Translate(user.Id, "invalid-email-confirmation")); } - _logger.LogInformation("User is updating email from {OldEmail} to {NewEmail}", user.Email, dto.Email); - var result = await _userManager.SetEmailAsync(user, dto.Email); + logger.LogInformation("User is updating email from {OldEmail} to {NewEmail}", user.Email, dto.Email.Sanitize()); + var result = await userManager.SetEmailAsync(user, dto.Email); if (!result.Succeeded) { - _logger.LogError("Unable to update email for users: {Errors}", result.Errors.Select(e => e.Description)); - return BadRequest(await _localizationService.Translate(user.Id, "generic-user-email-update")); + logger.LogError("Unable to update email for users: {Errors}", result.Errors.Select(e => e.Description)); + return BadRequest(await localizationService.Translate(user.Id, "generic-user-email-update")); } user.ConfirmationToken = null; user.EmailConfirmed = true; - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); // For the user's connected devices to pull the new information in - await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, + await eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id); return Ok(); @@ -931,7 +905,7 @@ public class AccountController : BaseApiController [HttpPost("confirm-password-reset")] public async Task> ConfirmForgotPassword(ConfirmPasswordResetDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); + var user = await unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); if (user == null) { return BadRequest(BadCredentialsMessage); @@ -939,21 +913,21 @@ public class AccountController : BaseApiController try { - var result = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, + var result = await userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, "ResetPassword", dto.Token); if (!result) { - _logger.LogInformation("Unable to reset password, your email token is not correct: {@Dto}", dto); + logger.LogInformation("Unable to reset password, your email token is not correct: {Token}", dto.Token.Sanitize()); return BadRequest(BadCredentialsMessage); } - var errors = await _accountService.ChangeUserPassword(user, dto.Password); - return errors.Any() ? BadRequest(errors) : Ok(await _localizationService.Translate(user.Id, "password-updated")); + var errors = await accountService.ChangeUserPassword(user, dto.Password); + return errors.Any() ? BadRequest(errors) : Ok(await localizationService.Translate(user.Id, "password-updated")); } catch (Exception ex) { - _logger.LogError(ex, "There was an unexpected error when confirming new password"); - return BadRequest(await _localizationService.Translate(user.Id, "generic-password-update")); + logger.LogError(ex, "There was an unexpected error when confirming new password"); + return BadRequest(await localizationService.Translate(user.Id, "generic-password-update")); } } @@ -968,58 +942,58 @@ public class AccountController : BaseApiController [EnableRateLimiting("Authentication")] public async Task> ForgotPassword([FromQuery] string email) { - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(email); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var user = await unitOfWork.UserRepository.GetUserByEmailAsync(email); if (user == null) { - _logger.LogError("There are no users with email: {Email} but user is requesting password reset", email); - return Ok(await _localizationService.Get("en", "forgot-password-generic")); + logger.LogError("There are no users with email: {Email} but user is requesting password reset", email.Sanitize().Censor()); + return Ok(await localizationService.Get("en", "forgot-password-generic")); } - var oidcConfig = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; + var oidcConfig = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; if (user.IdentityProvider == IdentityProvider.OpenIdConnect && oidcConfig is {Enabled: true, SyncUserSettings: true}) { - return BadRequest(await _localizationService.Translate(user.Id, "oidc-managed")); + return BadRequest(await localizationService.Translate(user.Id, "oidc-managed")); } - var roles = await _userManager.GetRolesAsync(user); + var roles = await userManager.GetRolesAsync(user); if (!roles.Any(r => r is PolicyConstants.AdminRole or PolicyConstants.ChangePasswordRole or PolicyConstants.ReadOnlyRole)) - return Unauthorized(await _localizationService.Translate(user.Id, "permission-denied")); + 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")); + return BadRequest(await localizationService.Translate(user.Id, "confirm-email")); - var token = await _userManager.GeneratePasswordResetTokenAsync(user); - var emailLink = await _emailService.GenerateEmailLink(Request, token, "confirm-reset-password", user.Email); + 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); + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(); + logger.LogCritical("[Forgot Password]: Email Link for {UserName}: {Link}", user.UserName, emailLink); - if (!settings.IsEmailSetup()) return Ok(await _localizationService.Get("en", "email-not-enabled")); - if (!_emailService.IsValidEmail(user.Email)) + if (!settings.IsEmailSetup()) return Ok(await localizationService.Get("en", "email-not-enabled")); + 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. Admin must change it in UI or from url above", user.Email); - return Ok(await _localizationService.Translate(user.Id, "invalid-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 or from url above", user.Email); + return Ok(await localizationService.Translate(user.Id, "invalid-email")); } - var installId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value; - BackgroundJob.Enqueue(() => _emailService.SendForgotPasswordEmail(new PasswordResetEmailDto() + 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")); + return Ok(await localizationService.Translate(user.Id, "email-sent")); } [HttpGet("email-confirmed")] public async Task> IsEmailConfirmed() { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); if (user == null) return Unauthorized(); return Ok(user.EmailConfirmed); @@ -1029,18 +1003,18 @@ public class AccountController : BaseApiController [HttpPost("confirm-migration-email")] public async Task> ConfirmMigrationEmail(ConfirmMigrationEmailDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); + var user = await unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); if (user == null) return BadRequest(BadCredentialsMessage); if (!await ConfirmEmailToken(dto.Token, user)) { - _logger.LogInformation("confirm-migration-email email token is invalid"); + logger.LogInformation("confirm-migration-email email token is invalid"); return BadRequest(BadCredentialsMessage); } - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); - user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(user.UserName!, + user = await unitOfWork.UserRepository.GetUserByUsernameAsync(user.UserName!, AppUserIncludes.UserPreferences | AppUserIncludes.AuthKeys); // Perform Login code @@ -1048,12 +1022,12 @@ public class AccountController : BaseApiController { Username = user!.UserName!, Email = user.Email!, - Token = await _tokenService.CreateToken(user), - RefreshToken = await _tokenService.CreateRefreshToken(user), + Token = await tokenService.CreateToken(user), + RefreshToken = await tokenService.CreateRefreshToken(user), ApiKey = user.GetOpdsAuthKey(), - AuthKeys = _mapper.Map>(user.AuthKeys), - Preferences = _mapper.Map(user.UserPreferences), - KavitaVersion = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value, + AuthKeys = mapper.Map>(user.AuthKeys), + Preferences = mapper.Map(user.UserPreferences), + KavitaVersion = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion)).Value, }; } @@ -1065,33 +1039,32 @@ public class AccountController : BaseApiController [Authorize(PolicyGroups.AdminPolicy)] [HttpPost("resend-confirmation-email")] [EnableRateLimiting("Authentication")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] 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")); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId); + if (user == null) return BadRequest(await localizationService.Get("en", "no-user")); if (string.IsNullOrEmpty(user.Email)) - return BadRequest( - await _localizationService.Translate(user.Id, "user-migration-needed")); - if (user.EmailConfirmed) return BadRequest(await _localizationService.Translate(user.Id, "user-already-confirmed")); + return BadRequest(await localizationService.Translate(user.Id, "user-migration-needed")); - // TODO: If the target user is read only, we might want to just forgo this + if (user.EmailConfirmed) return BadRequest(await localizationService.Translate(user.Id, "user-already-confirmed")); - var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); + var token = await userManager.GenerateEmailConfirmationTokenAsync(user); 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); + 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)) + 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); + 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); } - var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - var shouldEmailUser = serverSettings.IsEmailSetup() || !_emailService.IsValidEmail(user.Email); + var serverSettings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var shouldEmailUser = serverSettings.IsEmailSetup() || !emailService.IsValidEmail(user.Email); if (!shouldEmailUser) { @@ -1099,11 +1072,11 @@ public class AccountController : BaseApiController { EmailLink = emailLink, EmailSent = false, - InvalidEmail = !_emailService.IsValidEmail(user.Email) + InvalidEmail = !emailService.IsValidEmail(user.Email) }); } - BackgroundJob.Enqueue(() => _emailService.SendInviteEmail(new ConfirmationEmailDto() + BackgroundJob.Enqueue(() => emailService.SendInviteEmail(new ConfirmationEmailDto() { EmailAddress = user.Email!, InvitingUser = Username!, @@ -1115,21 +1088,21 @@ public class AccountController : BaseApiController { EmailLink = emailLink, EmailSent = true, - InvalidEmail = !_emailService.IsValidEmail(user.Email) + InvalidEmail = !emailService.IsValidEmail(user.Email) }); } private async Task ConfirmEmailToken(string token, AppUser user) { - var result = await _userManager.ConfirmEmailAsync(user, token); + var result = await userManager.ConfirmEmailAsync(user, token); if (result.Succeeded) return true; - _logger.LogCritical("[Account] Email validation failed"); + logger.LogCritical("[Account] Email validation failed"); if (!result.Errors.Any()) return false; foreach (var error in result.Errors) { - _logger.LogCritical("[Account] Email validation error: {Message}", error.Description); + logger.LogCritical("[Account] Email validation error: {Message}", error.Description); } return false; @@ -1142,8 +1115,8 @@ public class AccountController : BaseApiController [HttpGet("opds-url")] public async Task> GetOpdsUrl() { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId); - var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId); + var serverSettings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); var origin = HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value; if (!string.IsNullOrEmpty(serverSettings.HostName)) origin = serverSettings.HostName; @@ -1163,7 +1136,7 @@ public class AccountController : BaseApiController } } - var opdsAuthKey = (await _unitOfWork.UserRepository.GetAuthKeysForUserId(UserId)) + var opdsAuthKey = (await unitOfWork.UserRepository.GetAuthKeysForUserId(UserId)) .Where(k => k is {Name: AuthKeyHelper.OpdsKeyName, Provider: AuthKeyProvider.System}) .Select(k => k.Key) .FirstOrDefault(); @@ -1179,11 +1152,11 @@ public class AccountController : BaseApiController [HttpGet("is-email-valid")] public async Task> IsEmailValid() { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId); if (user == null) return Unauthorized(); if (string.IsNullOrEmpty(user.Email)) return Ok(false); - return Ok(_emailService.IsValidEmail(user.Email)); + return Ok(emailService.IsValidEmail(user.Email)); } /// @@ -1193,7 +1166,7 @@ public class AccountController : BaseApiController [HttpGet("auth-keys")] public async Task>> GetAuthKeys() { - return Ok(await _unitOfWork.UserRepository.GetAuthKeysForUserId(UserId)); + return Ok(await unitOfWork.UserRepository.GetAuthKeysForUserId(UserId)); } /// @@ -1206,7 +1179,7 @@ public class AccountController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> RotateAuthKey([FromQuery] int authKeyId, RotateAuthKeyRequestDto dto) { - var authKey = await _unitOfWork.UserRepository.GetAuthKeyById(authKeyId); + var authKey = await unitOfWork.UserRepository.GetAuthKeyById(authKeyId); if (authKey?.AppUserId != UserId) return BadRequest(); var oldKeyValue = authKey.Key; @@ -1220,13 +1193,13 @@ public class AccountController : BaseApiController authKey.Key = AuthKeyHelper.GenerateKey(dto.KeyLength); - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); - await _authKeyCacheInvalidator.InvalidateAsync(oldKeyValue); + await authKeyService.InvalidateAsync(oldKeyValue); - var newDto = _mapper.Map(authKey); + var newDto = mapper.Map(authKey); - await _eventHub.SendMessageToAsync(MessageFactory.AuthKeyUpdate, MessageFactory.AuthKeyUpdatedEvent(newDto), UserId); + await eventHub.SendMessageToAsync(MessageFactory.AuthKeyUpdate, MessageFactory.AuthKeyUpdatedEvent(newDto), UserId); return Ok(newDto); } @@ -1241,10 +1214,10 @@ public class AccountController : BaseApiController public async Task> CreateAuthKey(RotateAuthKeyRequestDto dto) { // Validate the name doesn't collide - var authKeys = await _unitOfWork.UserRepository.GetAuthKeysForUserId(UserId); + var authKeys = await unitOfWork.UserRepository.GetAuthKeysForUserId(UserId); if (authKeys.Any(k => string.Equals(k.Name, dto.Name, StringComparison.InvariantCultureIgnoreCase))) { - return BadRequest(await _localizationService.Translate(UserId, "auth-key-unique")); + return BadRequest(await localizationService.Translate(UserId, "auth-key-unique")); } var newKey = new AppUserAuthKey() @@ -1256,12 +1229,12 @@ public class AccountController : BaseApiController ExpiresAtUtc = string.IsNullOrEmpty(dto?.ExpiresUtc) ? null : DateTime.Parse(dto.ExpiresUtc), Provider = AuthKeyProvider.User, }; - _unitOfWork.UserRepository.Add(newKey); - await _unitOfWork.CommitAsync(); + unitOfWork.UserRepository.Add(newKey); + await unitOfWork.CommitAsync(); - var newDto = _mapper.Map(newKey); + var newDto = mapper.Map(newKey); - await _eventHub.SendMessageToAsync(MessageFactory.AuthKeyUpdate, MessageFactory.AuthKeyUpdatedEvent(newDto), UserId); + await eventHub.SendMessageToAsync(MessageFactory.AuthKeyUpdate, MessageFactory.AuthKeyUpdatedEvent(newDto), UserId); return Ok(newDto); } @@ -1275,14 +1248,14 @@ public class AccountController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task DeleteAuthKey(int authKeyId) { - var authKey = await _unitOfWork.UserRepository.GetAuthKeyById(authKeyId); + var authKey = await unitOfWork.UserRepository.GetAuthKeyById(authKeyId); if (authKey?.AppUserId != UserId) return BadRequest(); if (authKey.Provider != AuthKeyProvider.User) return BadRequest(); - _unitOfWork.UserRepository.Delete(authKey); - await _unitOfWork.CommitAsync(); + unitOfWork.UserRepository.Delete(authKey); + await unitOfWork.CommitAsync(); - await _eventHub.SendMessageToAsync(MessageFactory.AuthKeyDeleted, MessageFactory.AuthKeyDeletedEvent(authKeyId), UserId); + await eventHub.SendMessageToAsync(MessageFactory.AuthKeyDeleted, MessageFactory.AuthKeyDeletedEvent(authKeyId), UserId); return Ok(); } diff --git a/API/Controllers/ActivityController.cs b/Kavita.Server/Controllers/ActivityController.cs similarity index 82% rename from API/Controllers/ActivityController.cs rename to Kavita.Server/Controllers/ActivityController.cs index 35bb6bb26..ce38665d4 100644 --- a/API/Controllers/ActivityController.cs +++ b/Kavita.Server/Controllers/ActivityController.cs @@ -1,12 +1,12 @@ using System.Collections.Generic; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.Progress; +using Kavita.API.Database; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Progress; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; +namespace Kavita.Server.Controllers; public class ActivityController(IUnitOfWork unitOfWork) : BaseApiController { diff --git a/API/Controllers/AdminController.cs b/Kavita.Server/Controllers/AdminController.cs similarity index 84% rename from API/Controllers/AdminController.cs rename to Kavita.Server/Controllers/AdminController.cs index 3af60100b..083c47114 100644 --- a/API/Controllers/AdminController.cs +++ b/Kavita.Server/Controllers/AdminController.cs @@ -1,12 +1,12 @@ using System.Threading.Tasks; -using API.Constants; -using API.Entities; -using API.Middleware; +using Kavita.API.Attributes; +using Kavita.Models.Constants; +using Kavita.Models.Entities.User; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; +namespace Kavita.Server.Controllers; #nullable enable diff --git a/API/Controllers/AnnotationController.cs b/Kavita.Server/Controllers/AnnotationController.cs similarity index 94% rename from API/Controllers/AnnotationController.cs rename to Kavita.Server/Controllers/AnnotationController.cs index 82bcfc4c5..c2a8698e1 100644 --- a/API/Controllers/AnnotationController.cs +++ b/Kavita.Server/Controllers/AnnotationController.cs @@ -1,29 +1,25 @@ -#nullable enable -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.Metadata.Browse.Requests; -using API.DTOs.Reader; -using API.Extensions; -using API.Helpers; -using API.Middleware; -using API.Services; -using API.SignalR; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Common.Helpers; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Metadata.Browse.Requests; +using Kavita.Models.DTOs.Reader; +using Kavita.Server.Attributes; +using Kavita.Server.Extensions; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -namespace API.Controllers; +namespace Kavita.Server.Controllers; public class AnnotationController( IUnitOfWork unitOfWork, - ILogger logger, ILocalizationService localizationService, - IEventHub eventHub, IAnnotationService annotationService) : BaseApiController { @@ -226,6 +222,7 @@ public class AnnotationController( /// /// [HttpPost("export-filter")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task ExportAnnotationsFilter(BrowseAnnotationFilterDto filter, [FromQuery] UserParams? userParams) { userParams ??= UserParams.Default; @@ -247,6 +244,7 @@ public class AnnotationController( /// Export annotations with the given ids /// [HttpPost("export")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task ExportAnnotations(IList? annotations = null) { var json = await annotationService.ExportAnnotations(UserId, annotations); diff --git a/API/Controllers/BaseApiController.cs b/Kavita.Server/Controllers/BaseApiController.cs similarity index 98% rename from API/Controllers/BaseApiController.cs rename to Kavita.Server/Controllers/BaseApiController.cs index 87e9b9f42..624abbbe3 100644 --- a/API/Controllers/BaseApiController.cs +++ b/Kavita.Server/Controllers/BaseApiController.cs @@ -1,16 +1,14 @@ using System; -using System.Diagnostics; using System.IO; using System.Linq; using System.Security.Cryptography; -using System.Text; -using API.Services.Store; +using Kavita.API.Store; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using MimeTypes; -namespace API.Controllers; +namespace Kavita.Server.Controllers; #nullable enable diff --git a/API/Controllers/BookController.cs b/Kavita.Server/Controllers/BookController.cs similarity index 56% rename from API/Controllers/BookController.cs rename to Kavita.Server/Controllers/BookController.cs index e7985c9a0..07323728c 100644 --- a/API/Controllers/BookController.cs +++ b/Kavita.Server/Controllers/BookController.cs @@ -2,38 +2,28 @@ using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.Reader; -using API.Entities.Enums; -using API.Middleware; -using API.Services; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities.Enums; +using Kavita.Server.Attributes; +using Kavita.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using VersOne.Epub; -namespace API.Controllers; +namespace Kavita.Server.Controllers; -#nullable enable - -public class BookController : BaseApiController +public class BookController( + IBookService bookService, + IUnitOfWork unitOfWork, + ICacheService cacheService, + ILocalizationService localizationService) + : BaseApiController { - private readonly IBookService _bookService; - private readonly IUnitOfWork _unitOfWork; - private readonly ICacheService _cacheService; - private readonly ILocalizationService _localizationService; - - public BookController(IBookService bookService, - IUnitOfWork unitOfWork, ICacheService cacheService, - ILocalizationService localizationService) - { - _bookService = bookService; - _unitOfWork = unitOfWork; - _cacheService = cacheService; - _localizationService = localizationService; - } - /// /// Retrieves information for the PDF and Epub reader. This will cache the file. /// @@ -44,8 +34,8 @@ public class BookController : BaseApiController [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId"])] public async Task> GetBookInfo(int chapterId) { - var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId); - if (dto == null) return BadRequest(await _localizationService.Translate(UserId, "chapter-doesnt-exist")); + var dto = await unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId); + if (dto == null) return BadRequest(await localizationService.Translate(UserId, "chapter-doesnt-exist")); var bookTitle = string.Empty; @@ -53,19 +43,23 @@ public class BookController : BaseApiController { case MangaFormat.Epub: { - var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId))[0]; - await _cacheService.Ensure(chapterId); - var file = _cacheService.GetCachedFile(chapterId, mangaFile.FilePath); + var mangaFile = (await unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId))[0]; + await cacheService.Ensure(chapterId); + + var file = cacheService.GetCachedFile(chapterId, mangaFile.FilePath); using var book = await EpubReader.OpenBookAsync(file, BookService.LenientBookReaderOptions); + if (book == null) return NotFound(); + bookTitle = book.Title; break; } case MangaFormat.Pdf: { - var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId))[0]; - await _cacheService.Ensure(chapterId); - var file = _cacheService.GetCachedFile(chapterId, mangaFile.FilePath); + var mangaFile = (await unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId))[0]; + await cacheService.Ensure(chapterId); + + var file = cacheService.GetCachedFile(chapterId, mangaFile.FilePath); if (string.IsNullOrEmpty(bookTitle)) { // Override with filename @@ -105,21 +99,21 @@ public class BookController : BaseApiController /// /// /// - [AllowAnonymous] + [ChapterAccess] [SkipDeviceTracking] [HttpGet("{chapterId}/book-resources")] [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = ["chapterId", "file"])] public async Task GetBookPageResources(int chapterId, [FromQuery] string file) { - if (chapterId <= 0) return BadRequest(await _localizationService.Get("en", "chapter-doesnt-exist")); + if (chapterId <= 0) return BadRequest(await localizationService.Get("en", "chapter-doesnt-exist")); - var chapter = await _cacheService.Ensure(chapterId); - if (chapter == null) return BadRequest(await _localizationService.Get("en", "chapter-doesnt-exist")); + var chapter = await cacheService.Ensure(chapterId); + if (chapter == null) return BadRequest(await localizationService.Get("en", "chapter-doesnt-exist")); - var cachedFilePath = Path.Join(_cacheService.GetCachePath(chapterId), Path.GetFileName(chapter.Files.ElementAt(0).FilePath)); - var result = await _bookService.GetResourceAsync(cachedFilePath, file); + var cachedFilePath = Path.Join(cacheService.GetCachePath(chapterId), Path.GetFileName(chapter.Files.ElementAt(0).FilePath)); + var result = await bookService.GetResourceAsync(cachedFilePath, file); - if (!result.IsSuccess) return BadRequest(await _localizationService.Get("en", result.ErrorMessage)); + if (!result.IsSuccess) return BadRequest(await localizationService.Get("en", result.ErrorMessage)); return File(result.Content, result.ContentType, $"{chapterId}-{file}"); } @@ -134,13 +128,14 @@ public class BookController : BaseApiController [HttpGet("{chapterId}/chapters")] public async Task>> GetBookChapters(int chapterId) { - if (chapterId <= 0) return BadRequest(await _localizationService.Translate(UserId, "chapter-doesnt-exist")); - var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId); - if (chapter == null) return BadRequest(await _localizationService.Translate(UserId, "chapter-doesnt-exist")); + if (chapterId <= 0) return BadRequest(await localizationService.Translate(UserId, "chapter-doesnt-exist")); + + var chapter = await unitOfWork.ChapterRepository.GetChapterAsync(chapterId); + if (chapter == null) return BadRequest(await localizationService.Translate(UserId, "chapter-doesnt-exist")); try { - return Ok(await _bookService.GenerateTableOfContents(chapter)); + return Ok(await bookService.GenerateTableOfContents(chapter)); } catch (KavitaException ex) { @@ -159,23 +154,23 @@ public class BookController : BaseApiController [HttpGet("{chapterId}/book-page")] public async Task> GetBookPage(int chapterId, [FromQuery] int page) { - var chapter = await _cacheService.Ensure(chapterId); - if (chapter == null) return BadRequest(await _localizationService.Translate(UserId, "chapter-doesnt-exist")); - var path = _cacheService.GetCachedFile(chapter); + var chapter = await cacheService.Ensure(chapterId); + if (chapter == null) return BadRequest(await localizationService.Translate(UserId, "chapter-doesnt-exist")); + var path = cacheService.GetCachedFile(chapter); var baseUrl = "//" + Request.Host + Request.PathBase + "/api/"; try { var ptocBookmarks = - await _unitOfWork.UserTableOfContentRepository.GetPersonalToCForPage(UserId, chapterId, page); - var annotations = await _unitOfWork.UserRepository.GetAnnotationsByPage(UserId, chapter.Id, page); + await unitOfWork.UserTableOfContentRepository.GetPersonalToCForPage(UserId, chapterId, page); + var annotations = await unitOfWork.UserRepository.GetAnnotationsByPage(UserId, chapter.Id, page); - return Ok(await _bookService.GetBookPage(page, chapterId, path, baseUrl, ptocBookmarks, annotations)); + return Ok(await bookService.GetBookPage(UserId, page, chapterId, path, baseUrl, ptocBookmarks, annotations)); } catch (KavitaException ex) { - return BadRequest(await _localizationService.Translate(UserId, ex.Message)); + return BadRequest(await localizationService.Translate(UserId, ex.Message)); } } } diff --git a/API/Controllers/CBLController.cs b/Kavita.Server/Controllers/CBLController.cs similarity index 67% rename from API/Controllers/CBLController.cs rename to Kavita.Server/Controllers/CBLController.cs index 298681a80..1e0159674 100644 --- a/API/Controllers/CBLController.cs +++ b/Kavita.Server/Controllers/CBLController.cs @@ -2,34 +2,27 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; -using API.Constants; -using API.DTOs.ReadingLists.CBL; -using API.Middleware; -using API.Services; +using Kavita.API.Attributes; +using Kavita.API.Services; +using Kavita.API.Services.Reading; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.ReadingLists.CBL; +using Kavita.Server.Attributes; +using Kavita.Services.Reading; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Swashbuckle.AspNetCore.Annotations; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; /// /// Responsible for the CBL import flow /// -public class CblController : BaseApiController +public class CblController( + IReadingListService readingListService, + IDirectoryService directoryService) + : BaseApiController { - private readonly IReadingListService _readingListService; - private readonly IDirectoryService _directoryService; - private readonly ILocalizationService _localizationService; - - public CblController(IReadingListService readingListService, IDirectoryService directoryService, ILocalizationService localizationService) - { - _readingListService = readingListService; - _directoryService = directoryService; - _localizationService = localizationService; - } - /// /// The first step in a cbl import. This validates the cbl file that if an import occured, would it be successful. /// If this returns errors, the cbl will always be rejected by Kavita. @@ -45,39 +38,39 @@ public class CblController : BaseApiController try { var cblReadingList = await SaveAndLoadCblFile(cbl); - var importSummary = await _readingListService.ValidateCblFile(userId, cblReadingList, useComicVineMatching); + var importSummary = await readingListService.ValidateCblFile(userId, cblReadingList, useComicVineMatching); importSummary.FileName = cbl.FileName; return Ok(importSummary); } catch (ArgumentNullException) { - return Ok(new CblImportSummaryDto() + return Ok(new CblImportSummaryDto { FileName = cbl.FileName, Success = CblImportResult.Fail, - Results = new List() - { - new CblBookResult() + Results = + [ + new CblBookResult { Reason = CblImportReason.InvalidFile } - } + ] }); } catch (InvalidOperationException) { - return Ok(new CblImportSummaryDto() + return Ok(new CblImportSummaryDto { FileName = cbl.FileName, Success = CblImportResult.Fail, - Results = new List() - { - new CblBookResult() + Results = + [ + new CblBookResult { Reason = CblImportReason.InvalidFile } - } + ] }); } } @@ -99,38 +92,38 @@ public class CblController : BaseApiController { var userId = UserId; var cblReadingList = await SaveAndLoadCblFile(cbl); - var importSummary = await _readingListService.CreateReadingListFromCbl(userId, cblReadingList, dryRun, useComicVineMatching); + var importSummary = await readingListService.CreateReadingListFromCbl(userId, cblReadingList, dryRun, useComicVineMatching); importSummary.FileName = cbl.FileName; return Ok(importSummary); } catch (ArgumentNullException) { - return Ok(new CblImportSummaryDto() + return Ok(new CblImportSummaryDto { FileName = cbl.FileName, Success = CblImportResult.Fail, - Results = new List() - { - new CblBookResult() + Results = + [ + new CblBookResult { Reason = CblImportReason.InvalidFile } - } + ] }); } catch (InvalidOperationException) { - return Ok(new CblImportSummaryDto() + return Ok(new CblImportSummaryDto { FileName = cbl.FileName, Success = CblImportResult.Fail, - Results = new List() - { - new CblBookResult() + Results = + [ + new CblBookResult { Reason = CblImportReason.InvalidFile } - } + ] }); } @@ -139,7 +132,7 @@ public class CblController : BaseApiController private async Task SaveAndLoadCblFile(IFormFile file) { var filename = Path.GetRandomFileName(); - var outputFile = Path.Join(_directoryService.TempDirectory, filename); + var outputFile = Path.Join(directoryService.TempDirectory, filename); await using var stream = System.IO.File.Create(outputFile); await file.CopyToAsync(stream); stream.Close(); diff --git a/API/Controllers/ChapterController.cs b/Kavita.Server/Controllers/ChapterController.cs similarity index 70% rename from API/Controllers/ChapterController.cs rename to Kavita.Server/Controllers/ChapterController.cs index 081df91b9..b18e9c8b0 100644 --- a/API/Controllers/ChapterController.cs +++ b/Kavita.Server/Controllers/ChapterController.cs @@ -2,42 +2,33 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.Entities.Enums; -using API.Entities.MetadataMatching; -using API.Extensions; -using API.Helpers; -using API.Middleware; -using API.Services; -using API.SignalR; -using AutoMapper; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; +using Kavita.Common.Extensions; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.MetadataMatching; +using Kavita.Server.Attributes; +using Kavita.Services.Helpers; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Nager.ArticleNumber; -namespace API.Controllers; +namespace Kavita.Server.Controllers; -public class ChapterController : BaseApiController +public class ChapterController( + IUnitOfWork unitOfWork, + ILocalizationService localizationService, + IEventHub eventHub, + ILogger logger) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; - private readonly IEventHub _eventHub; - private readonly ILogger _logger; - private readonly IMapper _mapper; - - public ChapterController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IEventHub eventHub, ILogger logger, - IMapper mapper) - { - _unitOfWork = unitOfWork; - _localizationService = localizationService; - _eventHub = eventHub; - _logger = logger; - _mapper = mapper; - } /// /// Gets a single chapter @@ -45,9 +36,10 @@ public class ChapterController : BaseApiController /// /// [HttpGet] + [ChapterAccess] public async Task> GetChapter(int chapterId) { - var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId, UserId); + var chapter = await unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId, UserId); return Ok(chapter); } @@ -62,46 +54,46 @@ public class ChapterController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> DeleteChapter(int chapterId) { - var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId, + var chapter = await unitOfWork.ChapterRepository.GetChapterAsync(chapterId, ChapterIncludes.Files | ChapterIncludes.ExternalReviews | ChapterIncludes.ExternalRatings); if (chapter == null) - return BadRequest(_localizationService.Translate(UserId, "chapter-doesnt-exist")); + return BadRequest(localizationService.Translate(UserId, "chapter-doesnt-exist")); - var vol = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapter.VolumeId, VolumeIncludes.Chapters); - if (vol == null) return BadRequest(_localizationService.Translate(UserId, "volume-doesnt-exist")); + var vol = await unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapter.VolumeId, VolumeIncludes.Chapters); + if (vol == null) return BadRequest(localizationService.Translate(UserId, "volume-doesnt-exist")); // If there is only 1 chapter within the volume, then we need to remove the volume var needToRemoveVolume = vol.Chapters.Count == 1; if (needToRemoveVolume) { - _unitOfWork.VolumeRepository.Remove(vol); + unitOfWork.VolumeRepository.Remove(vol); } else { - _unitOfWork.ChapterRepository.Remove(chapter); + unitOfWork.ChapterRepository.Remove(chapter); } // If we removed the volume, do an additional check if we need to delete the actual series as well or not - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(vol.SeriesId, SeriesIncludes.ExternalData | SeriesIncludes.Volumes); + var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(vol.SeriesId, SeriesIncludes.ExternalData | SeriesIncludes.Volumes); var needToRemoveSeries = needToRemoveVolume && series != null && series.Volumes.Count <= 1; if (needToRemoveSeries) { - _unitOfWork.SeriesRepository.Remove(series!); + unitOfWork.SeriesRepository.Remove(series!); } - if (!await _unitOfWork.CommitAsync()) return Ok(false); + if (!await unitOfWork.CommitAsync()) return Ok(false); - await _eventHub.SendMessageAsync(MessageFactory.ChapterRemoved, MessageFactory.ChapterRemovedEvent(chapter.Id, vol.SeriesId), false); + await eventHub.SendMessageAsync(MessageFactory.ChapterRemoved, MessageFactory.ChapterRemovedEvent(chapter.Id, vol.SeriesId), false); if (needToRemoveVolume) { - await _eventHub.SendMessageAsync(MessageFactory.VolumeRemoved, MessageFactory.VolumeRemovedEvent(chapter.VolumeId, vol.SeriesId), false); + await eventHub.SendMessageAsync(MessageFactory.VolumeRemoved, MessageFactory.VolumeRemovedEvent(chapter.VolumeId, vol.SeriesId), false); } if (needToRemoveSeries) { - await _eventHub.SendMessageAsync(MessageFactory.SeriesRemoved, + await eventHub.SendMessageAsync(MessageFactory.SeriesRemoved, MessageFactory.SeriesRemovedEvent(series!.Id, series.Name, series.LibraryId), false); } @@ -114,8 +106,8 @@ public class ChapterController : BaseApiController /// The ID of the series /// The IDs of the chapters to be deleted /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("delete-multiple")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> DeleteMultipleChapters([FromQuery] int seriesId, DeleteChaptersDto dto) { try @@ -127,7 +119,7 @@ public class ChapterController : BaseApiController } // Fetch all chapters to be deleted - var chapters = (await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds)).ToList(); + var chapters = (await unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds)).ToList(); // Group chapters by their volume var volumesToUpdate = chapters.GroupBy(c => c.VolumeId).ToList(); @@ -139,38 +131,38 @@ public class ChapterController : BaseApiController var chaptersToDelete = volumeGroup.ToList(); // Fetch the volume - var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(volumeId, VolumeIncludes.Chapters); + var volume = await unitOfWork.VolumeRepository.GetVolumeByIdAsync(volumeId, VolumeIncludes.Chapters); if (volume == null) - return BadRequest(_localizationService.Translate(UserId, "volume-doesnt-exist")); + return BadRequest(localizationService.Translate(UserId, "volume-doesnt-exist")); // Check if all chapters in the volume are being deleted var isVolumeToBeRemoved = volume.Chapters.Count == chaptersToDelete.Count; if (isVolumeToBeRemoved) { - _unitOfWork.VolumeRepository.Remove(volume); + unitOfWork.VolumeRepository.Remove(volume); removedVolumes.Add(volume.Id); } else { // Remove only the specified chapters - _unitOfWork.ChapterRepository.Remove(chaptersToDelete); + unitOfWork.ChapterRepository.Remove(chaptersToDelete); } } - if (!await _unitOfWork.CommitAsync()) return Ok(false); + if (!await unitOfWork.CommitAsync()) return Ok(false); // Send events for removed chapters foreach (var chapter in chapters) { - await _eventHub.SendMessageAsync(MessageFactory.ChapterRemoved, + await eventHub.SendMessageAsync(MessageFactory.ChapterRemoved, MessageFactory.ChapterRemovedEvent(chapter.Id, seriesId), false); } // Send events for removed volumes foreach (var volumeId in removedVolumes) { - await _eventHub.SendMessageAsync(MessageFactory.VolumeRemoved, + await eventHub.SendMessageAsync(MessageFactory.VolumeRemoved, MessageFactory.VolumeRemovedEvent(volumeId, seriesId), false); } @@ -178,8 +170,8 @@ public class ChapterController : BaseApiController } catch (Exception ex) { - _logger.LogError(ex, "An error occured while deleting chapters"); - return BadRequest(_localizationService.Translate(UserId, "generic-error")); + logger.LogError(ex, "An error occured while deleting chapters"); + return BadRequest(localizationService.Translate(UserId, "generic-error")); } } @@ -190,14 +182,16 @@ public class ChapterController : BaseApiController /// /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("update")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task UpdateChapterMetadata(UpdateChapterDto dto) { - var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(dto.Id, - ChapterIncludes.People | ChapterIncludes.Genres | ChapterIncludes.Tags); + var chapter = await unitOfWork.ChapterRepository.GetChapterAsync(dto.Id, + ChapterIncludes.People | ChapterIncludes.Genres | ChapterIncludes.Tags, HttpContext.RequestAborted); if (chapter == null) - return BadRequest(_localizationService.Translate(UserId, "chapter-doesnt-exist")); + return BadRequest(localizationService.Translate(UserId, "chapter-doesnt-exist")); + + var seriesId = await unitOfWork.ChapterRepository.GetSeriesIdForChapter(chapter.Id, HttpContext.RequestAborted); if (chapter.AgeRating != dto.AgeRating) { @@ -256,12 +250,12 @@ public class ChapterController : BaseApiController #region Genres chapter.Genres ??= []; - await GenreHelper.UpdateChapterGenres(chapter, dto.Genres.Select(t => t.Title), _unitOfWork); + await GenreHelper.UpdateChapterGenres(chapter, dto.Genres.Select(t => t.Title), unitOfWork); #endregion #region Tags chapter.Tags ??= []; - await TagHelper.UpdateChapterTags(chapter, dto.Tags.Select(t => t.Title), _unitOfWork); + await TagHelper.UpdateChapterTags(chapter, dto.Tags.Select(t => t.Title), unitOfWork); #endregion #region People @@ -272,7 +266,7 @@ public class ChapterController : BaseApiController chapter, dto.Writers.Select(p => p.Name).ToList(), PersonRole.Writer, - _unitOfWork + unitOfWork ); // Update characters @@ -280,7 +274,7 @@ public class ChapterController : BaseApiController chapter, dto.Characters.Select(p => p.Name).ToList(), PersonRole.Character, - _unitOfWork + unitOfWork ); // Update pencillers @@ -288,7 +282,7 @@ public class ChapterController : BaseApiController chapter, dto.Pencillers.Select(p => p.Name).ToList(), PersonRole.Penciller, - _unitOfWork + unitOfWork ); // Update inkers @@ -296,7 +290,7 @@ public class ChapterController : BaseApiController chapter, dto.Inkers.Select(p => p.Name).ToList(), PersonRole.Inker, - _unitOfWork + unitOfWork ); // Update colorists @@ -304,7 +298,7 @@ public class ChapterController : BaseApiController chapter, dto.Colorists.Select(p => p.Name).ToList(), PersonRole.Colorist, - _unitOfWork + unitOfWork ); // Update letterers @@ -312,7 +306,7 @@ public class ChapterController : BaseApiController chapter, dto.Letterers.Select(p => p.Name).ToList(), PersonRole.Letterer, - _unitOfWork + unitOfWork ); // Update cover artists @@ -320,7 +314,7 @@ public class ChapterController : BaseApiController chapter, dto.CoverArtists.Select(p => p.Name).ToList(), PersonRole.CoverArtist, - _unitOfWork + unitOfWork ); // Update editors @@ -328,25 +322,26 @@ public class ChapterController : BaseApiController chapter, dto.Editors.Select(p => p.Name).ToList(), PersonRole.Editor, - _unitOfWork + unitOfWork ); - // TODO: Only remove field if changes were made - chapter.KPlusOverrides.Remove(MetadataSettingField.ChapterPublisher); // Update publishers - await PersonHelper.UpdateChapterPeopleAsync( + var updatedPublishers = await PersonHelper.UpdateChapterPeopleAsync( chapter, dto.Publishers.Select(p => p.Name).ToList(), PersonRole.Publisher, - _unitOfWork + unitOfWork ); + if (updatedPublishers) + chapter.KPlusOverrides.Remove(MetadataSettingField.ChapterPublisher); + // Update translators await PersonHelper.UpdateChapterPeopleAsync( chapter, dto.Translators.Select(p => p.Name).ToList(), PersonRole.Translator, - _unitOfWork + unitOfWork ); // Update imprints @@ -354,7 +349,7 @@ public class ChapterController : BaseApiController chapter, dto.Imprints.Select(p => p.Name).ToList(), PersonRole.Imprint, - _unitOfWork + unitOfWork ); // Update teams @@ -362,7 +357,7 @@ public class ChapterController : BaseApiController chapter, dto.Teams.Select(p => p.Name).ToList(), PersonRole.Team, - _unitOfWork + unitOfWork ); // Update locations @@ -370,7 +365,7 @@ public class ChapterController : BaseApiController chapter, dto.Locations.Select(p => p.Name).ToList(), PersonRole.Location, - _unitOfWork + unitOfWork ); #endregion @@ -398,17 +393,21 @@ public class ChapterController : BaseApiController #endregion - _unitOfWork.ChapterRepository.Update(chapter); + unitOfWork.ChapterRepository.Update(chapter); - if (!_unitOfWork.HasChanges()) + if (!unitOfWork.HasChanges()) { return Ok(); } - // TODO: Emit a ChapterMetadataUpdate out - - await _unitOfWork.CommitAsync(); + if (seriesId.HasValue) + { + await eventHub.SendMessageAsync(MessageFactory.ChapterUpdated, + MessageFactory.ChapterUpdatedEvent(chapter.Id, seriesId.Value), + false, HttpContext.RequestAborted); + } + await unitOfWork.CommitAsync(); return Ok(); } @@ -418,24 +417,25 @@ public class ChapterController : BaseApiController /// /// /// + [ChapterAccess] [HttpGet("chapter-detail-plus")] public async Task> ChapterDetailPlus([FromQuery] int chapterId) { var ret = new ChapterDetailPlusDto(); - var userReviews = (await _unitOfWork.UserRepository.GetUserRatingDtosForChapterAsync(chapterId, UserId)) + var userReviews = (await unitOfWork.UserRepository.GetUserRatingDtosForChapterAsync(chapterId, UserId)) .Where(r => !string.IsNullOrEmpty(r.Body)) .OrderByDescending(review => review.Username.Equals(Username!) ? 1 : 0) .ToList(); - var ownRating = await _unitOfWork.UserRepository.GetUserChapterRatingAsync(UserId, chapterId); + var ownRating = await unitOfWork.UserRepository.GetUserChapterRatingAsync(UserId, chapterId); if (ownRating != null) { ret.Rating = ownRating.Rating; ret.HasBeenRated = ownRating.HasBeenRated; } - var externalReviews = await _unitOfWork.ChapterRepository.GetExternalChapterReviewDtos(chapterId); + var externalReviews = await unitOfWork.ChapterRepository.GetExternalChapterReviewDtos(chapterId); if (externalReviews.Count > 0) { userReviews.AddRange(ReviewHelper.SelectSpectrumOfReviews(externalReviews)); @@ -443,7 +443,7 @@ public class ChapterController : BaseApiController ret.Reviews = userReviews; - ret.Ratings = await _unitOfWork.ChapterRepository.GetExternalChapterRatingDtos(chapterId); + ret.Ratings = await unitOfWork.ChapterRepository.GetExternalChapterRatingDtos(chapterId); return Ok(ret); } diff --git a/API/Controllers/CollectionController.cs b/Kavita.Server/Controllers/CollectionController.cs similarity index 56% rename from API/Controllers/CollectionController.cs rename to Kavita.Server/Controllers/CollectionController.cs index e333bd42a..06a5bd29e 100644 --- a/API/Controllers/CollectionController.cs +++ b/Kavita.Server/Controllers/CollectionController.cs @@ -2,53 +2,36 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Collection; -using API.DTOs.CollectionTags; -using API.Entities; -using API.Helpers.Builders; -using API.Middleware; -using API.Services; -using API.Services.Plus; -using API.SignalR; using Hangfire; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Models.Builders; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Collection; +using Kavita.Models.DTOs.CollectionTags; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Server.Attributes; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; /// /// APIs for Collections /// -public class CollectionController : BaseApiController +/// +public class CollectionController(IUnitOfWork unitOfWork, ICollectionTagService collectionService, + ILocalizationService localizationService, IExternalMetadataService externalMetadataService, + ISmartCollectionSyncService collectionSyncService, ILogger logger, + IEventHub eventHub) : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly ICollectionTagService _collectionService; - private readonly ILocalizationService _localizationService; - private readonly IExternalMetadataService _externalMetadataService; - private readonly ISmartCollectionSyncService _collectionSyncService; - private readonly ILogger _logger; - private readonly IEventHub _eventHub; - - /// - public CollectionController(IUnitOfWork unitOfWork, ICollectionTagService collectionService, - ILocalizationService localizationService, IExternalMetadataService externalMetadataService, - ISmartCollectionSyncService collectionSyncService, ILogger logger, - IEventHub eventHub) - { - _unitOfWork = unitOfWork; - _collectionService = collectionService; - _localizationService = localizationService; - _externalMetadataService = externalMetadataService; - _collectionSyncService = collectionSyncService; - _logger = logger; - _eventHub = eventHub; - } /// /// Returns all Collection tags for a given User @@ -57,7 +40,7 @@ public class CollectionController : BaseApiController [HttpGet] public async Task>> GetAllTags(bool ownedOnly = false) { - return Ok(await _unitOfWork.CollectionTagRepository.GetCollectionDtosAsync(UserId, !ownedOnly)); + return Ok(await unitOfWork.CollectionTagRepository.GetCollectionDtosAsync(UserId, !ownedOnly)); } /// @@ -68,8 +51,8 @@ public class CollectionController : BaseApiController [HttpGet("single")] public async Task> GetTag(int collectionId) { - var result = await _unitOfWork.CollectionTagRepository.GetCollectionDtoAsync(collectionId, UserId); - if (result == null) return NotFound(); // TODO: Figure out how to best handle restrictions/not found across the codebase + var result = await unitOfWork.CollectionTagRepository.GetCollectionDtoAsync(collectionId, UserId); + if (result == null) return NotFound(); return Ok(result); } @@ -83,7 +66,7 @@ public class CollectionController : BaseApiController [HttpGet("all-series")] public async Task>> GetCollectionsBySeries(int seriesId, bool ownedOnly = false) { - return Ok(await _unitOfWork.CollectionTagRepository.GetCollectionDtosBySeriesAsync(UserId, seriesId, !ownedOnly)); + return Ok(await unitOfWork.CollectionTagRepository.GetCollectionDtosBySeriesAsync(UserId, seriesId, !ownedOnly)); } @@ -95,7 +78,7 @@ public class CollectionController : BaseApiController [HttpGet("name-exists")] public async Task> DoesNameExists(string name) { - return Ok(await _unitOfWork.CollectionTagRepository.CollectionExists(name, UserId)); + return Ok(await unitOfWork.CollectionTagRepository.CollectionExists(name, UserId)); } /// @@ -110,19 +93,19 @@ public class CollectionController : BaseApiController { try { - if (await _collectionService.UpdateTag(updatedTag, UserId)) + if (await collectionService.UpdateTag(updatedTag, UserId)) { - await _eventHub.SendMessageAsync(MessageFactory.CollectionUpdated, + await eventHub.SendMessageAsync(MessageFactory.CollectionUpdated, MessageFactory.CollectionUpdatedEvent(updatedTag.Id), false); - return Ok(await _unitOfWork.CollectionTagRepository.GetCollectionDtoAsync(updatedTag.Id, UserId)); + return Ok(await unitOfWork.CollectionTagRepository.GetCollectionDtoAsync(updatedTag.Id, UserId)); } } catch (KavitaException ex) { - return BadRequest(await _localizationService.Translate(UserId, ex.Message)); + return BadRequest(await localizationService.Translate(UserId, ex.Message)); } - return BadRequest(await _localizationService.Translate(UserId, "generic-error")); + return BadRequest(await localizationService.Translate(UserId, "generic-error")); } /// @@ -135,23 +118,23 @@ public class CollectionController : BaseApiController public async Task PromoteMultipleCollections(PromoteCollectionsDto dto) { // This needs to take into account owner as I can select other users cards - var collections = await _unitOfWork.CollectionTagRepository.GetCollectionsByIds(dto.CollectionIds); + var collections = await unitOfWork.CollectionTagRepository.GetCollectionsByIds(dto.CollectionIds); var userId = UserId; if (!User.IsInRole(PolicyConstants.PromoteRole) && !User.IsInRole(PolicyConstants.AdminRole)) { - return BadRequest(await _localizationService.Translate(userId, "permission-denied")); + return BadRequest(await localizationService.Translate(userId, "permission-denied")); } foreach (var collection in collections) { if (collection.AppUserId != userId) continue; collection.Promoted = dto.Promoted; - _unitOfWork.CollectionTagRepository.Update(collection); + unitOfWork.CollectionTagRepository.Update(collection); } - if (!_unitOfWork.HasChanges()) return Ok(); - await _unitOfWork.CommitAsync(); + if (!unitOfWork.HasChanges()) return Ok(); + await unitOfWork.CommitAsync(); return Ok(); } @@ -167,14 +150,15 @@ public class CollectionController : BaseApiController public async Task DeleteMultipleCollections(DeleteCollectionsDto dto) { // This needs to take into account owner as I can select other users cards - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Collections); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Collections); if (user == null) return Unauthorized(); + user.Collections = user.Collections.Where(uc => !dto.CollectionIds.Contains(uc.Id)).ToList(); - _unitOfWork.UserRepository.Update(user); + unitOfWork.UserRepository.Update(user); - if (!_unitOfWork.HasChanges()) return Ok(); - await _unitOfWork.CommitAsync(); + if (!unitOfWork.HasChanges()) return Ok(); + await unitOfWork.CommitAsync(); return Ok(); } @@ -189,7 +173,7 @@ public class CollectionController : BaseApiController public async Task AddToMultipleSeries(CollectionTagBulkAddDto dto) { // Create a new tag and save - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Collections); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Collections); if (user == null) return Unauthorized(); AppUserCollection? tag; @@ -206,19 +190,19 @@ public class CollectionController : BaseApiController if (tag == null) { - return BadRequest(_localizationService.Translate(UserId, "collection-doesnt-exists")); + return BadRequest(localizationService.Translate(UserId, "collection-doesnt-exists")); } - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdsAsync(dto.SeriesIds.ToList(), false); + var series = await unitOfWork.SeriesRepository.GetSeriesByIdsAsync(dto.SeriesIds.ToList(), false); foreach (var s in series) { if (tag.Items.Contains(s)) continue; tag.Items.Add(s); } - _unitOfWork.UserRepository.Update(user); - if (await _unitOfWork.CommitAsync()) return Ok(); + unitOfWork.UserRepository.Update(user); + if (await unitOfWork.CommitAsync()) return Ok(); - return BadRequest(await _localizationService.Translate(UserId, "generic-error")); + return BadRequest(await localizationService.Translate(UserId, "generic-error")); } /// @@ -232,18 +216,18 @@ public class CollectionController : BaseApiController { try { - var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(updateSeriesForTagDto.Tag.Id, CollectionIncludes.Series); - if (tag == null) return BadRequest(await _localizationService.Translate(UserId, "collection-doesnt-exist")); + var tag = await unitOfWork.CollectionTagRepository.GetCollectionAsync(updateSeriesForTagDto.Tag.Id, CollectionIncludes.Series); + if (tag == null) return BadRequest(await localizationService.Translate(UserId, "collection-doesnt-exist")); - if (await _collectionService.RemoveTagFromSeries(tag, updateSeriesForTagDto.SeriesIdsToRemove)) - return Ok(await _localizationService.Translate(UserId, "collection-updated")); + if (await collectionService.RemoveTagFromSeries(tag, updateSeriesForTagDto.SeriesIdsToRemove)) + return Ok(await localizationService.Translate(UserId, "collection-updated")); } catch (Exception) { - await _unitOfWork.RollbackAsync(); + await unitOfWork.RollbackAsync(); } - return BadRequest(await _localizationService.Translate(UserId, "generic-error")); + return BadRequest(await localizationService.Translate(UserId, "generic-error")); } /// @@ -257,23 +241,24 @@ public class CollectionController : BaseApiController { try { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Collections); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Collections); if (user == null) return Unauthorized(); - if (user.Collections.All(c => c.Id != tagId)) - return BadRequest(await _localizationService.Translate(user.Id, "access-denied")); - if (await _collectionService.DeleteTag(tagId, user)) + if (user.Collections.All(c => c.Id != tagId)) + return BadRequest(await localizationService.Translate(user.Id, "access-denied")); + + if (await collectionService.DeleteTag(tagId, user)) { - return Ok(await _localizationService.Translate(UserId, "collection-deleted")); + return Ok(await localizationService.Translate(UserId, "collection-deleted")); } } catch (Exception ex) { - await _unitOfWork.RollbackAsync(); + await unitOfWork.RollbackAsync(); } - return BadRequest(await _localizationService.Translate(UserId, "generic-error")); + return BadRequest(await localizationService.Translate(UserId, "generic-error")); } /// @@ -285,7 +270,7 @@ public class CollectionController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task>> GetMalStacksForUser() { - return Ok(await _externalMetadataService.GetStacksForUser(UserId)); + return Ok(await externalMetadataService.GetStacksForUser(UserId)); } /// @@ -297,13 +282,13 @@ public class CollectionController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task ImportMalStack(MalStackDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Collections); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Collections); if (user == null) return Unauthorized(); // Validation check to ensure stack doesn't exist already - if (await _unitOfWork.CollectionTagRepository.CollectionExists(dto.Title, user.Id)) + if (await unitOfWork.CollectionTagRepository.CollectionExists(dto.Title, user.Id)) { - return BadRequest(_localizationService.Translate(user.Id, "collection-already-exists")); + return BadRequest(localizationService.Translate(user.Id, "collection-already-exists")); } try @@ -315,18 +300,18 @@ public class CollectionController : BaseApiController .Build(); user.Collections.Add(newCollection); - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(); // Trigger Stack Refresh for just one stack (not all) - BackgroundJob.Enqueue(() => _collectionSyncService.Sync(newCollection.Id)); + BackgroundJob.Enqueue(() => collectionSyncService.Sync(newCollection.Id)); return Ok(); } catch (Exception ex) { - _logger.LogError(ex, "There was an issue importing MAL Stack"); + logger.LogError(ex, "There was an issue importing MAL Stack"); } - return BadRequest(_localizationService.Translate(user.Id, "error-import-stack")); + return BadRequest(localizationService.Translate(user.Id, "error-import-stack")); } } diff --git a/API/Controllers/ColorScapeController.cs b/Kavita.Server/Controllers/ColorScapeController.cs similarity index 68% rename from API/Controllers/ColorScapeController.cs rename to Kavita.Server/Controllers/ColorScapeController.cs index 850e94122..7fd64434a 100644 --- a/API/Controllers/ColorScapeController.cs +++ b/Kavita.Server/Controllers/ColorScapeController.cs @@ -1,31 +1,26 @@ using System.Threading.Tasks; -using API.Data; -using API.DTOs.Theme; -using API.Entities.Interfaces; +using Kavita.API.Database; +using Kavita.Models.DTOs.Theme; +using Kavita.Models.Entities.Interfaces; +using Kavita.Server.Attributes; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; +namespace Kavita.Server.Controllers; [Authorize] -public class ColorScapeController : BaseApiController +public class ColorScapeController(IUnitOfWork unitOfWork) : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - - public ColorScapeController(IUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } - /// /// Returns the color scape for a series /// /// /// + [SeriesAccess] [HttpGet("series")] public async Task> GetColorScapeForSeries(int id) { - var entity = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(id, UserId); + var entity = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(id, UserId); return GetColorSpaceDto(entity); } @@ -34,10 +29,11 @@ public class ColorScapeController : BaseApiController /// /// /// + [VolumeAccess] [HttpGet("volume")] public async Task> GetColorScapeForVolume(int id) { - var entity = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(id, UserId); + var entity = await unitOfWork.VolumeRepository.GetVolumeDtoAsync(id, UserId); return GetColorSpaceDto(entity); } @@ -46,15 +42,16 @@ public class ColorScapeController : BaseApiController /// /// /// + [ChapterAccess] [HttpGet("chapter")] public async Task> GetColorScapeForChapter(int id) { - var entity = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(id, UserId); + var entity = await unitOfWork.ChapterRepository.GetChapterDtoAsync(id, UserId); return GetColorSpaceDto(entity); } - private ActionResult GetColorSpaceDto(IHasCoverImage entity) + private ActionResult GetColorSpaceDto(IHasCoverImage? entity) { if (entity == null) return Ok(ColorScapeDto.Empty); return Ok(new ColorScapeDto(entity.PrimaryColor, entity.SecondaryColor)); diff --git a/API/Controllers/DeprecatedController.cs b/Kavita.Server/Controllers/DeprecatedController.cs similarity index 74% rename from API/Controllers/DeprecatedController.cs rename to Kavita.Server/Controllers/DeprecatedController.cs index 55e0c19a0..e340013a8 100644 --- a/API/Controllers/DeprecatedController.cs +++ b/Kavita.Server/Controllers/DeprecatedController.cs @@ -2,49 +2,38 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs; -using API.DTOs.Account; -using API.DTOs.Filtering; -using API.DTOs.Metadata; -using API.DTOs.Progress; -using API.DTOs.Statistics; -using API.DTOs.Uploads; -using API.Extensions; -using API.Helpers; -using API.Services; using AutoMapper; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.Common.Helpers; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Account; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.DTOs.Statistics; +using Kavita.Models.DTOs.Uploads; +using Kavita.Server.Extensions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace API.Controllers; +namespace Kavita.Server.Controllers; /// /// All APIs here are subject to be removed and are no longer maintained. Will be removed v0.9.0 /// [Route("api/")] -public class DeprecatedController : BaseApiController +public class DeprecatedController( + IUnitOfWork unitOfWork, + ILocalizationService localizationService, + ITaskScheduler taskScheduler, + ILogger logger, + IStatisticService statService, + IMapper mapper) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; - private readonly ITaskScheduler _taskScheduler; - private readonly ILogger _logger; - private readonly IStatisticService _statService; - private readonly IMapper _mapper; - - public DeprecatedController(IUnitOfWork unitOfWork, ILocalizationService localizationService, ITaskScheduler taskScheduler, - ILogger logger, IStatisticService statService, IMapper mapper) - { - _unitOfWork = unitOfWork; - _localizationService = localizationService; - _taskScheduler = taskScheduler; - _logger = logger; - _statService = statService; - _mapper = mapper; - } - /// /// Return all Series that are in the current logged-in user's Want to Read list, filtered (deprecated, use v2) /// @@ -57,7 +46,7 @@ public class DeprecatedController : BaseApiController public async Task>> GetWantToRead([FromQuery] UserParams? userParams, FilterDto filterDto) { userParams ??= new UserParams(); - var pagedList = await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(UserId, userParams, filterDto); + var pagedList = await unitOfWork.SeriesRepository.GetWantToReadForUserAsync(UserId, userParams, filterDto); Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages); return Ok(pagedList); @@ -72,7 +61,7 @@ public class DeprecatedController : BaseApiController [HttpGet("series/chapter-metadata")] public async Task> GetChapterMetadata(int chapterId) { - return Ok(await _unitOfWork.ChapterRepository.GetChapterMetadataDtoAsync(chapterId)); + return Ok(await unitOfWork.ChapterRepository.GetChapterMetadataDtoAsync(chapterId)); } /// @@ -89,7 +78,7 @@ public class DeprecatedController : BaseApiController { var userId = UserId; var series = - await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, userParams, filterDto); + await unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, userParams, filterDto); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); @@ -110,7 +99,7 @@ public class DeprecatedController : BaseApiController { var userId = UserId; var series = - await _unitOfWork.SeriesRepository.GetRecentlyAdded(libraryId, userId, userParams, filterDto); + await unitOfWork.SeriesRepository.GetRecentlyAdded(libraryId, userId, userParams, filterDto); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); @@ -130,7 +119,7 @@ public class DeprecatedController : BaseApiController { var userId = UserId; var series = - await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, userParams, filterDto); + await unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, userParams, filterDto); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); @@ -149,36 +138,36 @@ public class DeprecatedController : BaseApiController { try { - var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id); - if (chapter == null) return BadRequest(await _localizationService.Translate(UserId, "chapter-doesnt-exist")); + var chapter = await unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id); + if (chapter == null) return BadRequest(await localizationService.Translate(UserId, "chapter-doesnt-exist")); var originalFile = chapter.CoverImage; chapter.CoverImage = string.Empty; chapter.CoverImageLocked = false; - _unitOfWork.ChapterRepository.Update(chapter); + unitOfWork.ChapterRepository.Update(chapter); - var volume = (await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapter.VolumeId))!; + var volume = (await unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapter.VolumeId))!; volume.CoverImage = chapter.CoverImage; - _unitOfWork.VolumeRepository.Update(volume); + unitOfWork.VolumeRepository.Update(volume); - var series = (await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId))!; + var series = (await unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId))!; - if (_unitOfWork.HasChanges()) + if (unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); if (originalFile != null) System.IO.File.Delete(originalFile); - await _taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id, true); + await taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id, true); return Ok(); } } catch (Exception e) { - _logger.LogError(e, "There was an issue resetting cover lock for Chapter {Id}", uploadFileDto.Id); - await _unitOfWork.RollbackAsync(); + logger.LogError(e, "There was an issue resetting cover lock for Chapter {Id}", uploadFileDto.Id); + await unitOfWork.RollbackAsync(); } - return BadRequest(await _localizationService.Translate(UserId, "reset-chapter-lock")); + return BadRequest(await localizationService.Translate(UserId, "reset-chapter-lock")); } @@ -187,11 +176,11 @@ public class DeprecatedController : BaseApiController [Obsolete("Will be removed in v0.9.0")] public async Task>> GetReadingHistory(int userId) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); var isAdmin = User.IsInRole(PolicyConstants.AdminRole); if (!isAdmin && userId != user!.Id) return BadRequest(); - return Ok(await _statService.GetReadingHistory(userId)); + return Ok(await statService.GetReadingHistory(userId)); } [Authorize(PolicyGroups.AdminPolicy)] @@ -200,7 +189,7 @@ public class DeprecatedController : BaseApiController [Obsolete("Will be removed in v0.9.0")] public async Task>>> GetTopYears() { - return Ok(await _statService.GetTopYears()); + return Ok(await statService.GetTopYears()); } /// @@ -214,11 +203,11 @@ public class DeprecatedController : BaseApiController [Obsolete("Will be removed in v0.9.0")] public async Task>>> ReadCountByDay(int userId = 0, int days = 0) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); var isAdmin = User.IsInRole(PolicyConstants.AdminRole); if (!isAdmin && userId != user!.Id) return BadRequest(); - return Ok(await _statService.ReadCountByDay(userId, days)); + return Ok(await statService.ReadCountByDay(userId, days)); } [Authorize(PolicyGroups.AdminPolicy)] @@ -227,7 +216,7 @@ public class DeprecatedController : BaseApiController [Obsolete("Will be removed in v0.9.0")] public async Task>>> GetYearStatistics() { - return Ok(await _statService.GetYearCount()); + return Ok(await statService.GetYearCount()); } /// @@ -241,7 +230,7 @@ public class DeprecatedController : BaseApiController [Obsolete("Will be removed in v0.9.0")] public async Task>> GetTopReads(int days = 0) { - return Ok(await _statService.GetTopUsers(days)); + return Ok(await statService.GetTopUsers(days)); } /// @@ -254,7 +243,7 @@ public class DeprecatedController : BaseApiController public async Task>> GetProgressForChapter(int chapterId) { var userId = User.IsInRole(PolicyConstants.AdminRole) ? 0 : UserId; - return Ok(await _unitOfWork.AppUserProgressRepository.GetUserProgressForChapter(chapterId, userId)); + return Ok(await unitOfWork.AppUserProgressRepository.GetUserProgressForChapter(chapterId, userId)); } /// @@ -268,7 +257,7 @@ public class DeprecatedController : BaseApiController public async Task>> GetQuickReads(int libraryId, [FromQuery] UserParams? userParams) { userParams ??= UserParams.Default; - var series = await _unitOfWork.SeriesRepository.GetQuickReads(UserId, libraryId, userParams); + var series = await unitOfWork.SeriesRepository.GetQuickReads(UserId, libraryId, userParams); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); return Ok(series); @@ -285,7 +274,7 @@ public class DeprecatedController : BaseApiController public async Task>> GetQuickCatchupReads(int libraryId, [FromQuery] UserParams? userParams) { userParams ??= UserParams.Default; - var series = await _unitOfWork.SeriesRepository.GetQuickCatchupReads(UserId, libraryId, userParams); + var series = await unitOfWork.SeriesRepository.GetQuickCatchupReads(UserId, libraryId, userParams); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); return Ok(series); @@ -304,7 +293,7 @@ public class DeprecatedController : BaseApiController var userId = UserId; userParams ??= UserParams.Default; - var series = await _unitOfWork.SeriesRepository.GetHighlyRated(userId, libraryId, userParams); + var series = await unitOfWork.SeriesRepository.GetHighlyRated(userId, libraryId, userParams); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); @@ -325,7 +314,7 @@ public class DeprecatedController : BaseApiController var userId = UserId; userParams ??= UserParams.Default; - var series = await _unitOfWork.SeriesRepository.GetMoreIn(userId, libraryId, genreId, userParams); + var series = await unitOfWork.SeriesRepository.GetMoreIn(userId, libraryId, genreId, userParams); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); return Ok(series); @@ -342,7 +331,7 @@ public class DeprecatedController : BaseApiController public async Task>> GetRediscover(int libraryId, [FromQuery] UserParams? userParams) { userParams ??= UserParams.Default; - var series = await _unitOfWork.SeriesRepository.GetRediscover(UserId, libraryId, userParams); + var series = await unitOfWork.SeriesRepository.GetRediscover(UserId, libraryId, userParams); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); return Ok(series); @@ -352,8 +341,8 @@ public class DeprecatedController : BaseApiController [HttpGet("users/myself")] public async Task>> GetMyself() { - var users = await _unitOfWork.UserRepository.GetAllUsersAsync(); - return Ok(users.Where(u => u.UserName == Username!).DefaultIfEmpty().Select(u => _mapper.Map(u)).SingleOrDefault()); + var users = await unitOfWork.UserRepository.GetAllUsersAsync(); + return Ok(users.Where(u => u.UserName == Username!).DefaultIfEmpty().Select(u => mapper.Map(u)).SingleOrDefault()); } } diff --git a/Kavita.Server/Controllers/DeviceController.cs b/Kavita.Server/Controllers/DeviceController.cs new file mode 100644 index 000000000..2bedd6c7b --- /dev/null +++ b/Kavita.Server/Controllers/DeviceController.cs @@ -0,0 +1,246 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AutoMapper; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; +using Kavita.Common; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Device.ClientDevice; +using Kavita.Models.DTOs.Device.EmailDevice; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.DTOs.SignalR; +using Kavita.Server.Attributes; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Kavita.Server.Controllers; + +/// +/// Responsible for interacting and creating Devices +/// +public class DeviceController( + IUnitOfWork unitOfWork, + IDeviceService deviceService, + IEventHub eventHub, + ILocalizationService localizationService, + IMapper mapper, + IClientDeviceService clientDeviceService) + : BaseApiController +{ + /// + /// Creates a new Device + /// + /// + /// + [HttpPost("create")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] + public async Task> CreateOrUpdateDevice(CreateEmailDeviceDto dto) + { + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Devices); + if (user == null) return Unauthorized(); + try + { + var device = await deviceService.Create(dto, user); + if (device == null) + return BadRequest(await localizationService.Translate(UserId, "generic-device-create")); + + return Ok(mapper.Map(device)); + } + catch (KavitaException ex) + { + return BadRequest(await localizationService.Translate(UserId, ex.Message)); + } + } + + /// + /// Updates an existing Device + /// + /// + /// + [HttpPost("update")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] + public async Task> UpdateDevice(UpdateEmailDeviceDto dto) + { + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Devices); + if (user == null) return Unauthorized(); + + var device = await deviceService.Update(dto, user); + if (device == null) return BadRequest(await localizationService.Translate(UserId, "generic-device-update")); + + return Ok(mapper.Map(device)); + } + + /// + /// Deletes the device from the user + /// + /// + /// + [HttpDelete] + [DisallowRole(PolicyConstants.ReadOnlyRole)] + public async Task DeleteDevice(int deviceId) + { + if (deviceId <= 0) return BadRequest(await localizationService.Translate(UserId, "device-doesnt-exist")); + + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Devices); + if (user == null) return Unauthorized(); + + if (await deviceService.Delete(user, deviceId)) return Ok(); + + return BadRequest(await localizationService.Translate(UserId, "generic-device-delete")); + } + + [HttpGet] + public async Task>> GetDevices() + { + return Ok(await unitOfWork.DeviceRepository.GetDevicesForUserAsync(UserId)); + } + + /// + /// Sends a collection of chapters to the user's device + /// + /// + /// + [HttpPost("send-to")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] + public async Task SendToDevice(SendToEmailDeviceDto dto) + { + var userId = UserId; + if (dto.ChapterIds.Any(i => i < 0)) return BadRequest(await localizationService.Translate(userId, "greater-0", "ChapterIds")); + if (dto.DeviceId < 0) return BadRequest(await localizationService.Translate(userId, "greater-0", "DeviceId")); + + var isEmailSetup = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).IsEmailSetupForSendToDevice(); + if (!isEmailSetup) + return BadRequest(await localizationService.Translate(userId, "send-to-kavita-email")); + + // // Validate that the device belongs to the user + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.Devices); + if (user == null || user.Devices.All(d => d.Id != dto.DeviceId)) return BadRequest(await localizationService.Translate(userId, "send-to-unallowed")); + + await eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, + MessageFactory.SendingToDeviceEvent(await localizationService.Translate(userId, "send-to-device-status"), + "started"), userId); + try + { + var success = await deviceService.SendTo(dto.ChapterIds, dto.DeviceId); + if (success) return Ok(); + } + catch (KavitaException ex) + { + return BadRequest(await localizationService.Translate(userId, ex.Message)); + } + finally + { + await eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, + MessageFactory.SendingToDeviceEvent(await localizationService.Translate(userId, "send-to-device-status"), + "ended"), userId); + } + + return BadRequest(await localizationService.Translate(userId, "generic-send-to")); + } + + + /// + /// Attempts to send a whole series to a device. + /// + /// + /// + [HttpPost("send-series-to")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] + public async Task SendSeriesToDevice(SendSeriesToEmailDeviceDto dto) + { + var userId = UserId; + if (dto.SeriesId <= 0) return BadRequest(await localizationService.Translate(userId, "greater-0", "SeriesId")); + if (dto.DeviceId < 0) return BadRequest(await localizationService.Translate(userId, "greater-0", "DeviceId")); + + var isEmailSetup = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).IsEmailSetupForSendToDevice(); + if (!isEmailSetup) + return BadRequest(await localizationService.Translate(userId, "send-to-kavita-email")); + + await eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, + MessageFactory.SendingToDeviceEvent(await localizationService.Translate(userId, "send-to-device-status"), + "started"), userId); + + var series = + await unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, + SeriesIncludes.Volumes | SeriesIncludes.Chapters); + if (series == null) return BadRequest(await localizationService.Translate(userId, "series-doesnt-exist")); + var chapterIds = series.Volumes.SelectMany(v => v.Chapters.Select(c => c.Id)).ToList(); + try + { + var success = await deviceService.SendTo(chapterIds, dto.DeviceId); + if (success) return Ok(); + } + catch (KavitaException ex) + { + return BadRequest(await localizationService.Translate(userId, ex.Message)); + } + finally + { + await eventHub.SendMessageToAsync(MessageFactory.NotificationProgress, + MessageFactory.SendingToDeviceEvent(await localizationService.Translate(userId, "send-to-device-status"), + "ended"), userId); + } + + return BadRequest(await localizationService.Translate(userId, "generic-send-to")); + } + + #region Client Devices + /// + /// Get my client devices + /// + /// + /// + [HttpGet("client/devices")] + public async Task>> GetMyClientDevices(bool includeInactive = false) + { + return Ok(await unitOfWork.ClientDeviceRepository.GetUserDeviceDtosAsync(UserId, includeInactive)); + } + + /// + /// Get All user client devices + /// + /// + /// + [Authorize(PolicyGroups.AdminPolicy)] + [HttpGet("client/all-devices")] + public async Task>> GetAllClientDevices(bool includeInactive = false) + { + return Ok(await unitOfWork.ClientDeviceRepository.GetAllUserDeviceDtos(includeInactive)); + } + + + /// + /// Removes the client device from DB + /// + /// + /// + [HttpDelete("client/device")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] + public async Task> DeleteClientDevice(int clientDeviceId) + { + return Ok(await clientDeviceService.DeleteDeviceAsync(UserId, clientDeviceId)); + } + + /// + /// Update the friendly name of the Device + /// + /// + /// + [HttpPost("client/update-name")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] + public async Task UpdateClientDeviceName(UpdateClientDeviceNameDto dto) + { + await clientDeviceService.UpdateFriendlyNameAsync(UserId, dto); + return Ok(); + } + + + + #endregion Client Devices + +} + + diff --git a/API/Controllers/DownloadController.cs b/Kavita.Server/Controllers/DownloadController.cs similarity index 54% rename from API/Controllers/DownloadController.cs rename to Kavita.Server/Controllers/DownloadController.cs index 9729441ad..08df315e0 100644 --- a/API/Controllers/DownloadController.cs +++ b/Kavita.Server/Controllers/DownloadController.cs @@ -3,63 +3,49 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.Downloads; -using API.Entities; -using API.Entities.Enums; -using API.Services; -using API.SignalR; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Downloads; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Server.Attributes; +using Kavita.Services.Extensions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; /// /// All APIs related to downloading entities from the system. Requires Download Role or Admin Role. /// [Authorize(PolicyGroups.DownloadPolicy)] -public class DownloadController : BaseApiController +public class DownloadController( + IUnitOfWork unitOfWork, + IArchiveService archiveService, + IDownloadService downloadService, + IEventHub eventHub, + ILogger logger, + IBookmarkService bookmarkService, + ILocalizationService localizationService) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly IArchiveService _archiveService; - private readonly IDirectoryService _directoryService; - private readonly IDownloadService _downloadService; - private readonly IEventHub _eventHub; - private readonly ILogger _logger; - private readonly IBookmarkService _bookmarkService; - private readonly IAccountService _accountService; - private readonly ILocalizationService _localizationService; private const string DefaultContentType = "application/octet-stream"; - public DownloadController(IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService, - IDownloadService downloadService, IEventHub eventHub, ILogger logger, IBookmarkService bookmarkService, - IAccountService accountService, ILocalizationService localizationService) - { - _unitOfWork = unitOfWork; - _archiveService = archiveService; - _directoryService = directoryService; - _downloadService = downloadService; - _eventHub = eventHub; - _logger = logger; - _bookmarkService = bookmarkService; - _accountService = accountService; - _localizationService = localizationService; - } - /// /// For a given volume, return the size in bytes /// /// /// + [VolumeAccess] [HttpGet("volume-size")] public async Task> GetVolumeSize(int volumeId) { - return Ok(await _unitOfWork.VolumeRepository.GetFilesizeForVolumeAsync(volumeId)); + return Ok(await unitOfWork.VolumeRepository.GetFilesizeForVolumeAsync(volumeId)); } /// @@ -70,7 +56,7 @@ public class DownloadController : BaseApiController [HttpPost("bulk-volume-size")] public async Task>> GetBulkVolumeSize([FromBody] IList volumeIds) { - return Ok(await _unitOfWork.VolumeRepository.GetFilesizeForVolumesAsync(volumeIds)); + return Ok(await unitOfWork.VolumeRepository.GetFilesizeForVolumesAsync(volumeIds)); } /// @@ -78,10 +64,11 @@ public class DownloadController : BaseApiController /// /// /// + [ChapterAccess] [HttpGet("chapter-size")] public async Task> GetChapterSize(int chapterId) { - return Ok(await _unitOfWork.ChapterRepository.GetFilesizeForChapterAsync(chapterId)); + return Ok(await unitOfWork.ChapterRepository.GetFilesizeForChapterAsync(chapterId)); } /// @@ -93,7 +80,7 @@ public class DownloadController : BaseApiController public async Task>> GetChapterSizeInBulk([FromBody] IList chapterIds) { // If there are more than 50 chapterIds, we need to break up into multiple calls - return Ok(await _unitOfWork.ChapterRepository.GetFilesizeForChaptersAsync(chapterIds)); + return Ok(await unitOfWork.ChapterRepository.GetFilesizeForChaptersAsync(chapterIds)); } /// @@ -101,10 +88,11 @@ public class DownloadController : BaseApiController /// /// /// + [SeriesAccess] [HttpGet("series-size")] public async Task> GetSeriesSize(int seriesId) { - return Ok(await _unitOfWork.SeriesRepository.GetFilesizeForSeriesAsync(seriesId)); + return Ok(await unitOfWork.SeriesRepository.GetFilesizeForSeriesAsync(seriesId)); } /// @@ -115,7 +103,7 @@ public class DownloadController : BaseApiController [HttpPost("bulk-series-size")] public async Task>> GetBulkSeriesSize([FromBody] IList seriesIds) { - return Ok(await _unitOfWork.SeriesRepository.GetFilesizeForMultipleSeriesAsync(seriesIds)); + return Ok(await unitOfWork.SeriesRepository.GetFilesizeForMultipleSeriesAsync(seriesIds)); } @@ -124,15 +112,17 @@ public class DownloadController : BaseApiController /// /// /// - [Authorize(PolicyGroups.DownloadPolicy)] + [VolumeAccess] [HttpGet("volume")] + [Authorize(PolicyGroups.DownloadPolicy)] public async Task DownloadVolume(int volumeId, [FromQuery] string? correlationId = null) { - if (!await HasDownloadPermission()) return BadRequest(await _localizationService.Translate(UserId, "permission-denied")); - var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(volumeId); - if (volume == null) return BadRequest(await _localizationService.Translate(UserId, "volume-doesnt-exist")); - var files = await _unitOfWork.VolumeRepository.GetFilesForVolume(volumeId); - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId); + var volume = await unitOfWork.VolumeRepository.GetVolumeByIdAsync(volumeId); + if (volume == null) return BadRequest(await localizationService.Translate(UserId, "volume-doesnt-exist")); + + var files = await unitOfWork.VolumeRepository.GetFilesForVolume(volumeId); + var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId); + try { return await DownloadFiles(files, $"download_{Username!}_v{volumeId}", $"{series!.Name} - Volume {volume.Name}.zip", correlationId); @@ -143,16 +133,9 @@ public class DownloadController : BaseApiController } } - private async Task HasDownloadPermission() - { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); - if (user == null) return false; - return await _accountService.HasDownloadPermission(user); - } - private PhysicalFileResult GetFirstFileDownload(IEnumerable files) { - var (zipFile, contentType, fileDownloadName) = _downloadService.GetFirstFileDownload(files); + var (zipFile, contentType, fileDownloadName) = downloadService.GetFirstFileDownload(files); return PhysicalFile(zipFile, contentType, fileDownloadName, true); } @@ -161,18 +144,23 @@ public class DownloadController : BaseApiController /// /// /// + [ChapterAccess] [HttpGet("chapter")] public async Task DownloadChapter(int chapterId, [FromQuery] string? correlationId = null) { - if (!await HasDownloadPermission()) return BadRequest(await _localizationService.Translate(UserId, "permission-denied")); - var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId); - var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId); - if (chapter == null) return BadRequest(await _localizationService.Translate(UserId, "chapter-doesnt-exist")); - var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapter.VolumeId); - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume!.SeriesId); + var files = await unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId); + var chapter = await unitOfWork.ChapterRepository.GetChapterAsync(chapterId); + if (chapter == null) return BadRequest(await localizationService.Translate(UserId, "chapter-doesnt-exist")); + + var volume = await unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapter.VolumeId); + var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume!.SeriesId); + try { - return await DownloadFiles(files, $"download_{Username!}_c{chapterId}", $"{series!.Name} - Chapter {chapter.GetNumberTitle()}.zip", correlationId); + return await DownloadFiles(files, + $"download_{Username!}_c{chapterId}", + $"{series!.Name} - Chapter {chapter.GetNumberTitle()}.zip", + correlationId); } catch (KavitaException ex) { @@ -186,7 +174,7 @@ public class DownloadController : BaseApiController var filename = Path.GetFileNameWithoutExtension(downloadName); try { - await _eventHub.SendMessageAsync(MessageFactory.DownloadProgress, + await eventHub.SendMessageAsync(MessageFactory.DownloadProgress, MessageFactory.DownloadProgressEvent(username, filename, $"Downloading {filename}", 0F, "started", correlationId)); @@ -195,47 +183,47 @@ public class DownloadController : BaseApiController // Emit "ended" after the response is fully sent to the client HttpContext.Response.OnCompleted(async () => { - await _eventHub.SendMessageAsync(MessageFactory.DownloadProgress, + await eventHub.SendMessageAsync(MessageFactory.DownloadProgress, MessageFactory.DownloadProgressEvent(username, filename, "Download Complete", 1F, "ended", correlationId)); }); return GetFirstFileDownload(files); } - var filePath = _archiveService.CreateZipFromFoldersForDownload(files.Select(c => c.FilePath).ToList(), tempFolder, ProgressCallback); + var filePath = archiveService.CreateZipFromFoldersForDownload(files.Select(c => c.FilePath).ToList(), tempFolder, ProgressCallback); - await _eventHub.SendMessageAsync(MessageFactory.DownloadProgress, + await eventHub.SendMessageAsync(MessageFactory.DownloadProgress, MessageFactory.DownloadProgressEvent(username, filename, "Download Complete", 1F, "ended", correlationId)); - return PhysicalFile(filePath, DefaultContentType, downloadName, true); + return PhysicalFile(filePath, DefaultContentType, Uri.EscapeDataString(downloadName), true); async Task ProgressCallback(Tuple progressInfo) { - await _eventHub.SendMessageAsync(MessageFactory.DownloadProgress, + await eventHub.SendMessageAsync(MessageFactory.DownloadProgress, MessageFactory.DownloadProgressEvent(username, filename, $"Processing {Path.GetFileNameWithoutExtension(progressInfo.Item1)}", - Math.Clamp(progressInfo.Item2, 0F, 1F), "updated", correlationId)); + Math.Clamp(progressInfo.Item2, 0F, 1F), correlationId)); } } catch (Exception ex) { - _logger.LogError(ex, "There was an exception when trying to download files"); - await _eventHub.SendMessageAsync(MessageFactory.DownloadProgress, + logger.LogError(ex, "There was an exception when trying to download files"); + await eventHub.SendMessageAsync(MessageFactory.DownloadProgress, MessageFactory.DownloadProgressEvent(Username!, filename, "Download Complete", 1F, "ended", correlationId)); throw; } } + [SeriesAccess] [HttpGet("series")] + [Authorize(PolicyGroups.DownloadPolicy)] public async Task DownloadSeries(int seriesId, [FromQuery] string? correlationId = null) { - if (!await HasDownloadPermission()) return BadRequest(await _localizationService.Translate(UserId, "permission-denied")); - - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); + var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); if (series == null) return BadRequest("Invalid Series"); - var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId); + var files = await unitOfWork.SeriesRepository.GetFilesForSeries(seriesId); try { return await DownloadFiles(files, $"download_{Username!}_s{seriesId}", $"{series.Name}.zip", correlationId); @@ -252,25 +240,32 @@ public class DownloadController : BaseApiController /// /// [HttpPost("bookmarks")] + [Authorize(PolicyGroups.DownloadPolicy)] public async Task DownloadBookmarkPages(DownloadBookmarkDto downloadBookmarkDto) { - if (!await HasDownloadPermission()) return BadRequest(await _localizationService.Translate(UserId, "permission-denied")); - if (!downloadBookmarkDto.Bookmarks.Any()) return BadRequest(await _localizationService.Translate(UserId, "bookmarks-empty")); + if (downloadBookmarkDto.Bookmarks.DistinctBy(b => b.SeriesId).Count() > 1) + return BadRequest(); + + var seriesId = downloadBookmarkDto.Bookmarks.First().SeriesId; + if (!await unitOfWork.UserRepository.HasAccessToSeries(UserId, seriesId, HttpContext.RequestAborted)) + return NotFound(); + + if (!downloadBookmarkDto.Bookmarks.Any()) return BadRequest(await localizationService.Translate(UserId, "bookmarks-empty")); - // We know that all bookmarks will be for one single seriesId var userId = UserId; var username = Username!; - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(downloadBookmarkDto.Bookmarks.First().SeriesId); + var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); - var files = await _bookmarkService.GetBookmarkFilesById(downloadBookmarkDto.Bookmarks.Select(b => b.Id)); + var files = await bookmarkService.GetBookmarkFilesById(downloadBookmarkDto.Bookmarks.Select(b => b.Id)); var filename = $"{series!.Name} - Bookmarks.zip"; - await _eventHub.SendMessageAsync(MessageFactory.DownloadProgress, + + await eventHub.SendMessageAsync(MessageFactory.DownloadProgress, 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.DownloadProgress, + + var filePath = archiveService.CreateZipForDownload(files,$"download_{userId}_{seriesId}_bookmarks"); + + await eventHub.SendMessageAsync(MessageFactory.DownloadProgress, MessageFactory.DownloadProgressEvent(username, Path.GetFileNameWithoutExtension(filename), $"Downloading {filename}", 1F)); diff --git a/Kavita.Server/Controllers/EmailController.cs b/Kavita.Server/Controllers/EmailController.cs new file mode 100644 index 000000000..d075961a0 --- /dev/null +++ b/Kavita.Server/Controllers/EmailController.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Kavita.API.Database; +using Kavita.Common.Helpers; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Email; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Kavita.Server.Controllers; + +[Authorize(Policy = PolicyGroups.AdminPolicy)] +public class EmailController(IUnitOfWork unitOfWork) : BaseApiController +{ + [HttpGet("all")] + public async Task>> GetEmails() + { + return Ok(await unitOfWork.EmailHistoryRepository.GetEmailDtos(UserParams.Default)); + } +} diff --git a/Kavita.Server/Controllers/FallbackController.cs b/Kavita.Server/Controllers/FallbackController.cs new file mode 100644 index 000000000..29012ba99 --- /dev/null +++ b/Kavita.Server/Controllers/FallbackController.cs @@ -0,0 +1,24 @@ +using System.IO; +using Kavita.API.Attributes; +using Kavita.API.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Kavita.Server.Controllers; + +[AllowAnonymous] +public class FallbackController : Controller +{ + + [SkipDeviceTracking] + public IActionResult Index() + { + if (HttpContext.Request.Path.StartsWithSegments("/api")) + { + return NotFound(); + } + + return PhysicalFile(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "index.html"), "text/HTML"); + } +} + diff --git a/API/Controllers/FilterController.cs b/Kavita.Server/Controllers/FilterController.cs similarity index 55% rename from API/Controllers/FilterController.cs rename to Kavita.Server/Controllers/FilterController.cs index cdd4bc2a5..ad2c9e236 100644 --- a/API/Controllers/FilterController.cs +++ b/Kavita.Server/Controllers/FilterController.cs @@ -2,37 +2,30 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Dashboard; -using API.DTOs.Filtering.v2; -using API.Entities; -using API.Helpers; -using API.Middleware; -using API.Services; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.Models; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Dashboard; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.Entities.User; +using Kavita.Server.Attributes; +using Kavita.Services; +using Kavita.Services.Helpers; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace API.Controllers; -#nullable enable +namespace Kavita.Server.Controllers; -public class FilterController : BaseApiController +public class FilterController( + IUnitOfWork unitOfWork, + ILocalizationService localizationService, + IStreamService streamService, + ILogger logger) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; - private readonly IStreamService _streamService; - private readonly ILogger _logger; - - public FilterController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IStreamService streamService, - ILogger logger) - { - _unitOfWork = unitOfWork; - _localizationService = localizationService; - _streamService = streamService; - _logger = logger; - } - /// /// Creates or Updates the filter /// @@ -42,11 +35,11 @@ public class FilterController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task CreateOrUpdateSmartFilter(FilterV2Dto dto) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.SmartFilters); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.SmartFilters); if (user == null) return Unauthorized(); if (string.IsNullOrWhiteSpace(dto.Name)) return BadRequest("Name must be set"); - if (Seed.DefaultStreams.Any(s => s.Name.Equals(dto.Name, StringComparison.InvariantCultureIgnoreCase))) + if (Defaults.DefaultStreams.Any(s => s.Name.Equals(dto.Name, StringComparison.InvariantCultureIgnoreCase))) { return BadRequest("You cannot use the name of a system provided stream"); } @@ -57,7 +50,7 @@ public class FilterController : BaseApiController { // Update the filter existingFilter.Filter = SmartFilterHelper.Encode(dto); - _unitOfWork.AppUserSmartFilterRepository.Update(existingFilter); + unitOfWork.AppUserSmartFilterRepository.Update(existingFilter); } else { @@ -67,11 +60,11 @@ public class FilterController : BaseApiController Filter = SmartFilterHelper.Encode(dto) }; user.SmartFilters.Add(existingFilter); - _unitOfWork.UserRepository.Update(user); + unitOfWork.UserRepository.Update(user); } - if (!_unitOfWork.HasChanges()) return Ok(); - await _unitOfWork.CommitAsync(); + if (!unitOfWork.HasChanges()) return Ok(); + await unitOfWork.CommitAsync(); return Ok(); } @@ -81,9 +74,9 @@ public class FilterController : BaseApiController /// /// [HttpGet] - public ActionResult> GetFilters() + public async Task>> GetFilters() { - return Ok(_unitOfWork.AppUserSmartFilterRepository.GetAllDtosByUserId(UserId)); + return Ok(await unitOfWork.AppUserSmartFilterRepository.GetAllDtosByUserId(UserId)); } /// @@ -96,17 +89,17 @@ public class FilterController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task DeleteFilter(int filterId) { - var filter = await _unitOfWork.AppUserSmartFilterRepository.GetById(filterId); + var filter = await unitOfWork.AppUserSmartFilterRepository.GetById(filterId); if (filter == null) return Ok(); // This needs to delete any dashboard filters that have it too - var streams = await _unitOfWork.UserRepository.GetDashboardStreamWithFilter(filter.Id); - _unitOfWork.UserRepository.Delete(streams); + var streams = await unitOfWork.UserRepository.GetDashboardStreamWithFilter(filter.Id); + unitOfWork.UserRepository.Delete(streams); - var streams2 = await _unitOfWork.UserRepository.GetSideNavStreamWithFilter(filter.Id); - _unitOfWork.UserRepository.Delete(streams2); + var streams2 = await unitOfWork.UserRepository.GetSideNavStreamWithFilter(filter.Id); + unitOfWork.UserRepository.Delete(streams2); - _unitOfWork.AppUserSmartFilterRepository.Delete(filter); - await _unitOfWork.CommitAsync(); + unitOfWork.AppUserSmartFilterRepository.Delete(filter); + await unitOfWork.CommitAsync(); return Ok(); } @@ -144,7 +137,7 @@ public class FilterController : BaseApiController { try { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.SmartFilters); if (user == null) return Unauthorized(); @@ -152,31 +145,31 @@ public class FilterController : BaseApiController if (string.IsNullOrWhiteSpace(name)) { - return BadRequest(await _localizationService.Translate(user.Id, "smart-filter-name-required")); + return BadRequest(await localizationService.Translate(user.Id, "smart-filter-name-required")); } - if (Seed.DefaultStreams.Any(s => s.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase))) + if (Defaults.DefaultStreams.Any(s => s.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase))) { - return BadRequest(await _localizationService.Translate(user.Id, "smart-filter-system-name")); + return BadRequest(await localizationService.Translate(user.Id, "smart-filter-system-name")); } var filter = user.SmartFilters.FirstOrDefault(f => f.Id == filterId); if (filter == null) { - return BadRequest(await _localizationService.Translate(user.Id, "filter-not-found")); + return BadRequest(await localizationService.Translate(user.Id, "filter-not-found")); } filter.Name = name; - _unitOfWork.AppUserSmartFilterRepository.Update(filter); - await _unitOfWork.CommitAsync(); + unitOfWork.AppUserSmartFilterRepository.Update(filter); + await unitOfWork.CommitAsync(); - await _streamService.RenameSmartFilterStreams(filter); + await streamService.RenameSmartFilterStreams(filter); return Ok(); } catch (Exception ex) { - _logger.LogError(ex, "There was an exception when renaming smart filter: {FilterId}", filterId); - return BadRequest(await _localizationService.Translate(UserId, "generic-error")); + logger.LogError(ex, "There was an exception when renaming smart filter: {FilterId}", filterId); + return BadRequest(await localizationService.Translate(UserId, "generic-error")); } } diff --git a/API/Controllers/FontController.cs b/Kavita.Server/Controllers/FontController.cs similarity index 59% rename from API/Controllers/FontController.cs rename to Kavita.Server/Controllers/FontController.cs index 7402d2608..624225a46 100644 --- a/API/Controllers/FontController.cs +++ b/Kavita.Server/Controllers/FontController.cs @@ -2,44 +2,33 @@ using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.Font; -using API.Entities.Enums.Font; -using API.Middleware; -using API.Services; -using API.Services.Tasks; -using API.Services.Tasks.Scanner.Parser; using AutoMapper; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Font; +using Kavita.Models.Entities.Enums.Font; +using Kavita.Server.Attributes; +using Kavita.Services.Scanner; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using MimeTypes; -namespace API.Controllers; +namespace Kavita.Server.Controllers; [Authorize] -public class FontController : BaseApiController +public class FontController( + IUnitOfWork unitOfWork, + IDirectoryService directoryService, + IFontService fontService, + IMapper mapper, + ILocalizationService localizationService) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly IDirectoryService _directoryService; - private readonly IFontService _fontService; - private readonly IMapper _mapper; - private readonly ILocalizationService _localizationService; - private readonly Regex _fontFileExtensionRegex = new(Parser.FontFileExtensions, RegexOptions.IgnoreCase, Parser.RegexTimeout); - public FontController(IUnitOfWork unitOfWork, IDirectoryService directoryService, - IFontService fontService, IMapper mapper, ILocalizationService localizationService) - { - _unitOfWork = unitOfWork; - _directoryService = directoryService; - _fontService = fontService; - _mapper = mapper; - _localizationService = localizationService; - } - /// /// List out the fonts /// @@ -47,29 +36,24 @@ public class FontController : BaseApiController [HttpGet("all")] public async Task>> GetFonts() { - return Ok(await _unitOfWork.EpubFontRepository.GetFontDtosAsync()); + return Ok(await unitOfWork.EpubFontRepository.GetFontDtosAsync()); } /// /// Returns a font file /// /// - /// /// [HttpGet] - [AllowAnonymous] [SkipDeviceTracking] - public async Task GetFont(int fontId, string apiKey) + public async Task GetFont(int fontId) { - var userId = await _unitOfWork.UserRepository.GetUserIdByAuthKeyAsync(apiKey); - if (userId == 0) return BadRequest(); - - var font = await _unitOfWork.EpubFontRepository.GetFontAsync(fontId); + var font = await unitOfWork.EpubFontRepository.GetFontAsync(fontId); if (font == null) return NotFound(); if (font.Provider == FontProvider.System) return BadRequest("System provided fonts are not loaded by API"); - var path = Path.Join(_directoryService.EpubFontDirectory, font.FileName); + var path = Path.Join(directoryService.EpubFontDirectory, font.FileName); return CachedFile(path); } @@ -85,10 +69,10 @@ public class FontController : BaseApiController public async Task DeleteFont(int fontId, bool force = false) { var forceDelete = User.IsInRole(PolicyConstants.AdminRole) && force; - var fontInUse = await _fontService.IsFontInUse(fontId); + var fontInUse = await fontService.IsFontInUse(fontId); if (!fontInUse || forceDelete) { - await _fontService.Delete(fontId); + await fontService.Delete(fontId); } return Ok(); @@ -102,7 +86,7 @@ public class FontController : BaseApiController [HttpGet("in-use")] public async Task> IsFontInUse(int fontId) { - return Ok(await _fontService.IsFontInUse(fontId)); + return Ok(await fontService.IsFontInUse(fontId)); } /// @@ -120,8 +104,8 @@ public class FontController : BaseApiController var tempFile = await UploadToTemp(formFile); - var font = await _fontService.CreateFontFromFileAsync(tempFile); - return Ok(_mapper.Map(font)); + var font = await fontService.CreateFontFromFileAsync(tempFile); + return Ok(mapper.Map(font)); } [HttpPost("upload-by-url")] @@ -131,18 +115,18 @@ public class FontController : BaseApiController // Validate url try { - var font = await _fontService.CreateFontFromUrl(url); - return Ok(_mapper.Map(font)); + var font = await fontService.CreateFontFromUrl(url); + return Ok(mapper.Map(font)); } catch (KavitaException ex) { - return BadRequest(_localizationService.Translate(UserId, ex.Message)); + return BadRequest(localizationService.Translate(UserId, ex.Message)); } } private async Task UploadToTemp(IFormFile file) { - var outputFile = Path.Join(_directoryService.TempDirectory, file.FileName); + var outputFile = Path.Join(directoryService.TempDirectory, file.FileName); await using var stream = System.IO.File.Create(outputFile); await file.CopyToAsync(stream); diff --git a/API/Controllers/HealthController.cs b/Kavita.Server/Controllers/HealthController.cs similarity index 86% rename from API/Controllers/HealthController.cs rename to Kavita.Server/Controllers/HealthController.cs index 63a5f27d8..cb7ec5e0d 100644 --- a/API/Controllers/HealthController.cs +++ b/Kavita.Server/Controllers/HealthController.cs @@ -1,13 +1,11 @@ -using API.Middleware; +using Kavita.API.Attributes; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; +namespace Kavita.Server.Controllers; -#nullable enable - -[SkipDeviceTracking] [AllowAnonymous] +[SkipDeviceTracking] public class HealthController : BaseApiController { /// diff --git a/Kavita.Server/Controllers/ImageController.cs b/Kavita.Server/Controllers/ImageController.cs new file mode 100644 index 000000000..d19fca007 --- /dev/null +++ b/Kavita.Server/Controllers/ImageController.cs @@ -0,0 +1,263 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Metadata; +using Kavita.API.Services.Reading; +using Kavita.Models.Constants; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Extensions; +using Kavita.Server.Attributes; +using Kavita.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Kavita.Server.Controllers; + +/// +/// Responsible for servicing up images stored in Kavita for entities +/// +/// +[SkipDeviceTracking] +public class ImageController(IUnitOfWork unitOfWork, IDirectoryService directoryService, + ILocalizationService localizationService, IReadingListService readingListService, + ICoverDbService coverDbService, ICollectionTagService collectionTagService) : BaseApiController +{ + + /// + /// Returns cover image for Chapter + /// + /// + /// + /// + [ChapterAccess] + [HttpGet("chapter-cover")] + public async Task GetChapterCoverImage(int chapterId, string apiKey) + { + var path = Path.Join(directoryService.CoverImageDirectory, await unitOfWork.ChapterRepository.GetChapterCoverImageAsync(chapterId)); + return PhysicalFile(path); + } + + /// + /// Returns cover image for Library + /// + /// + /// + /// + [LibraryAccess] + [HttpGet("library-cover")] + public async Task GetLibraryCoverImage(int libraryId, string apiKey) + { + var path = Path.Join(directoryService.CoverImageDirectory, await unitOfWork.LibraryRepository.GetLibraryCoverImageAsync(libraryId)); + return PhysicalFile(path); + } + + /// + /// Returns cover image for Volume + /// + /// + /// + /// + [VolumeAccess] + [HttpGet("volume-cover")] + public async Task GetVolumeCoverImage(int volumeId, string apiKey) + { + var path = Path.Join(directoryService.CoverImageDirectory, await unitOfWork.VolumeRepository.GetVolumeCoverImageAsync(volumeId)); + return PhysicalFile(path); + } + + /// + /// Returns cover image for Series + /// + /// Id of Series + /// + /// + [SeriesAccess] + [HttpGet("series-cover")] + public async Task GetSeriesCoverImage(int seriesId, string apiKey) + { + var path = Path.Join(directoryService.CoverImageDirectory, await unitOfWork.SeriesRepository.GetSeriesCoverImageAsync(seriesId)); + return PhysicalFile(path); + } + + /// + /// Returns cover image for Collection + /// + /// + /// + /// + [HttpGet("collection-cover")] + public async Task GetCollectionCoverImage(int collectionTagId, string apiKey) + { + var collectionTag = await unitOfWork.CollectionTagRepository.GetCollectionAsync(collectionTagId, ct: HttpContext.RequestAborted); + if (collectionTag == null || (collectionTag.AppUserId != UserId && !collectionTag.Promoted)) return NotFound(); + + var path = Path.Join(directoryService.CoverImageDirectory, collectionTag.CoverImage); + if (string.IsNullOrEmpty(path) || !directoryService.FileSystem.File.Exists(path)) + { + path = await collectionTagService.GenerateCollectionCoverImage(collectionTagId); + } + + return PhysicalFile(path); + } + + /// + /// Returns cover image for a Reading List + /// + /// + /// + /// + [HttpGet("readinglist-cover")] + public async Task GetReadingListCoverImage(int readingListId, string apiKey) + { + var readingList = await unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListId, ct: HttpContext.RequestAborted); + if (readingList == null || (readingList.AppUserId != UserId && !readingList.Promoted)) return NotFound(); + + var path = Path.Join(directoryService.CoverImageDirectory, readingList.CoverImage); + if (string.IsNullOrEmpty(path) || !directoryService.FileSystem.File.Exists(path)) + { + path = await readingListService.GenerateReadingListCoverImage(readingListId); + } + + return PhysicalFile(path); + } + + /// + /// Returns image for a given bookmark page + /// + /// This request is served unauthenticated, but user must be passed via api key to validate + /// + /// Starts at 0 + /// API Key for user. Needed to authenticate request + /// Only applicable for Epubs - handles multiple images on one page + /// + [ChapterAccess] + [HttpGet("bookmark")] + public async Task GetBookmarkImage(int chapterId, int pageNum, string apiKey, int imageOffset = 0) + { + var bookmark = await unitOfWork.UserRepository.GetBookmarkForPage(pageNum, chapterId, imageOffset, UserId); + if (bookmark == null) return BadRequest(await localizationService.Translate(UserId, "bookmark-doesnt-exist")); + + var bookmarkDirectory = + (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; + var path = Path.Join(bookmarkDirectory, bookmark.FileName); + + return PhysicalFile(path); + } + + /// + /// Returns the image associated with a web-link + /// + /// + /// + /// + [HttpGet("web-link")] + public async Task GetWebLinkImage(string url, string apiKey) + { + if (string.IsNullOrEmpty(url)) return BadRequest(await localizationService.Translate(UserId, "must-be-defined", "Url")); + + var encodeFormat = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; + + // Check if the domain exists + var domainFilePath = directoryService.FileSystem.Path.Join(directoryService.FaviconDirectory, ImageService.GetWebLinkFormat(url, encodeFormat)); + if (!directoryService.FileSystem.File.Exists(domainFilePath)) + { + // We need to request the favicon and save it + try + { + domainFilePath = directoryService.FileSystem.Path.Join(directoryService.FaviconDirectory, + await coverDbService.DownloadFaviconAsync(url, encodeFormat)); + } + catch (Exception) + { + return BadRequest(await localizationService.Translate(UserId, "generic-favicon")); + } + } + + return PhysicalFile(domainFilePath); + } + + + /// + /// Returns the image associated with a publisher + /// + /// + /// + /// + [HttpGet("publisher")] + public async Task GetPublisherImage(string publisherName, string apiKey) + { + if (string.IsNullOrEmpty(publisherName)) return BadRequest(await localizationService.Translate(UserId, "must-be-defined", "publisherName")); + if (publisherName.Contains("..")) return BadRequest(); + + var encodeFormat = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; + + // Check if the domain exists + var domainFilePath = directoryService.FileSystem.Path.Join(directoryService.PublisherDirectory, ImageService.GetPublisherFormat(publisherName, encodeFormat)); + if (!directoryService.FileSystem.File.Exists(domainFilePath)) + { + // We need to request the favicon and save it + try + { + domainFilePath = directoryService.FileSystem.Path.Join(directoryService.PublisherDirectory, + await coverDbService.DownloadPublisherImageAsync(publisherName, encodeFormat)); + } + catch (Exception) + { + return BadRequest(await localizationService.Translate(UserId, "generic-favicon")); + } + } + + return CachedFile(domainFilePath); + } + + /// + /// Returns cover image for Person + /// + /// + /// + /// + [PersonAccess] + [HttpGet("person-cover")] + public async Task GetPersonCoverImage(int personId, string apiKey) + { + var path = Path.Join(directoryService.CoverImageDirectory, await unitOfWork.UserRepository.GetPersonCoverImageAsync(personId)); + return PhysicalFile(path); + } + + /// + /// Returns cover image for User + /// + /// + /// + /// + [HttpGet("user-cover")] + public async Task GetUserCoverImage(int userId, string apiKey) + { + var filename = await unitOfWork.UserRepository.GetCoverImageAsync(userId); + if (filename == null) return NotFound(); + + var path = Path.Join(directoryService.CoverImageDirectory, filename); + return CachedFile(path); + } + + /// + /// Returns a temp coverupload image + /// + /// Requires Admin Role to perform upload + /// Filename of file. This is used with upload/upload-by-url + /// + /// + [HttpGet("cover-upload")] + [Authorize(PolicyConstants.AdminRole)] + public async Task GetCoverUploadImage(string filename, string apiKey) + { + if (filename.Contains("..")) return BadRequest(await localizationService.Translate(UserId, "invalid-filename")); + + var path = Path.Join(directoryService.TempDirectory, filename); + return PhysicalFile(path); + } +} diff --git a/API/Controllers/KoreaderController.cs b/Kavita.Server/Controllers/KoreaderController.cs similarity index 73% rename from API/Controllers/KoreaderController.cs rename to Kavita.Server/Controllers/KoreaderController.cs index 42b46ad6d..60982970a 100644 --- a/API/Controllers/KoreaderController.cs +++ b/Kavita.Server/Controllers/KoreaderController.cs @@ -1,16 +1,13 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; using System; using System.Threading.Tasks; -using API.Data; -using API.DTOs.Koreader; -using API.Extensions; -using API.Services; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs.Koreader; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace API.Controllers; -#nullable enable +namespace Kavita.Server.Controllers; /// /// The endpoint to interface with Koreader's Progress Sync plugin. @@ -19,17 +16,9 @@ namespace API.Controllers; /// Koreader uses a different form of authentication. It stores the username and password in headers. /// https://github.com/koreader/koreader/blob/master/plugins/kosync.koplugin/KOSyncClient.lua /// -public class KoreaderController : BaseApiController +public class KoreaderController(IKoreaderService koreaderService, ILogger logger) + : BaseApiController { - private readonly IKoreaderService _koreaderService; - private readonly ILogger _logger; - - public KoreaderController(IKoreaderService koreaderService, ILogger logger) - { - _koreaderService = koreaderService; - _logger = logger; - } - [HttpGet("{apiKey}/users/auth")] public IActionResult Authenticate(string apiKey) { @@ -47,7 +36,7 @@ public class KoreaderController : BaseApiController { try { - await _koreaderService.SaveProgress(request, UserId); + await koreaderService.SaveProgress(request, UserId); return Ok(new KoreaderProgressUpdateDto{ Document = request.document, Timestamp = DateTime.UtcNow }); } @@ -68,8 +57,8 @@ public class KoreaderController : BaseApiController { try { - var response = await _koreaderService.GetProgress(ebookHash, UserId); - _logger.LogDebug("Koreader response progress for User ({UserName}): {Progress}", Username, response.progress.Sanitize()); + var response = await koreaderService.GetProgress(ebookHash, UserId); + logger.LogDebug("Koreader response progress for User ({UserName}): {Progress}", Username, response.progress.Sanitize()); // We must pack this manually for Koreader due to a bug in their code: https://github.com/koreader/koreader/issues/13629 diff --git a/API/Controllers/LibraryController.cs b/Kavita.Server/Controllers/LibraryController.cs similarity index 64% rename from API/Controllers/LibraryController.cs rename to Kavita.Server/Controllers/LibraryController.cs index c9e864de1..b7a98fa98 100644 --- a/API/Controllers/LibraryController.cs +++ b/Kavita.Server/Controllers/LibraryController.cs @@ -3,80 +3,67 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Account; -using API.DTOs.JumpBar; -using API.DTOs.System; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Extensions; -using API.Helpers.Builders; -using API.Services; -using API.Services.Tasks.Scanner; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; using AutoMapper; using EasyCaching.Core; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Scanner; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models.Builders; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Account; +using Kavita.Models.DTOs.JumpBar; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.DTOs.System; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.User; +using Kavita.Models.Extensions; +using Kavita.Server.Attributes; +using Kavita.Services.Scanner; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using TaskScheduler = API.Services.TaskScheduler; +using TaskScheduler = Kavita.Services.TaskScheduler; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; [Authorize] -public class LibraryController : BaseApiController +public class LibraryController( + IDirectoryService directoryService, + ILogger logger, + IMapper mapper, + ITaskScheduler taskScheduler, + IUnitOfWork unitOfWork, + IEventHub eventHub, + ILibraryWatcher libraryWatcher, + IEasyCachingProviderFactory cachingProviderFactory, + ILocalizationService localizationService) + : BaseApiController { - private readonly IDirectoryService _directoryService; - private readonly ILogger _logger; - private readonly IMapper _mapper; - private readonly ITaskScheduler _taskScheduler; - private readonly IUnitOfWork _unitOfWork; - private readonly IEventHub _eventHub; - private readonly ILibraryWatcher _libraryWatcher; - private readonly ILocalizationService _localizationService; - private readonly IEasyCachingProvider _libraryCacheProvider; + private readonly IEasyCachingProvider _libraryCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.Library); private const string CacheKey = "library_"; - public LibraryController(IDirectoryService directoryService, - ILogger logger, IMapper mapper, ITaskScheduler taskScheduler, - IUnitOfWork unitOfWork, IEventHub eventHub, ILibraryWatcher libraryWatcher, - IEasyCachingProviderFactory cachingProviderFactory, ILocalizationService localizationService) - { - _directoryService = directoryService; - _logger = logger; - _mapper = mapper; - _taskScheduler = taskScheduler; - _unitOfWork = unitOfWork; - _eventHub = eventHub; - _libraryWatcher = libraryWatcher; - _localizationService = localizationService; - - _libraryCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.Library); - } - /// /// Creates a new Library. Upon library creation, adds new library to all Admin accounts. /// /// /// Created Library - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("create")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> AddLibrary(UpdateLibraryDto dto) { - if (await _unitOfWork.LibraryRepository.LibraryExists(dto.Name)) + if (await unitOfWork.LibraryRepository.LibraryExists(dto.Name)) { - return BadRequest(await _localizationService.Translate(UserId, "library-name-exists")); + return BadRequest(await localizationService.Translate(UserId, "library-name-exists")); } var library = new LibraryBuilder(dto.Name, dto.Type) @@ -105,64 +92,65 @@ public class LibraryController : BaseApiController // Override Scrobbling for Comic libraries since there are no providers to scrobble to if (library.Type == LibraryType.Comic) { - _logger.LogInformation("Overrode Library {Name} to disable scrobbling since there are no providers for Comics", dto.Name); + logger.LogInformation("Overrode Library {Name} to disable scrobbling since there are no providers for Comics", dto.Name.Sanitize()); library.AllowScrobbling = false; } - _unitOfWork.LibraryRepository.Add(library); + unitOfWork.LibraryRepository.Add(library); - var admins = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).ToList(); + var admins = (await unitOfWork.UserRepository.GetAdminUsersAsync()).ToList(); foreach (var admin in admins) { admin.Libraries ??= new List(); admin.Libraries.Add(library); } - if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(UserId, "generic-library")); - _logger.LogInformation("Created a new library: {LibraryName}", library.Name); + if (!await unitOfWork.CommitAsync()) return BadRequest(await localizationService.Translate(UserId, "generic-library")); + + logger.LogInformation("Created a new library: {LibraryName}", library.Name.Sanitize()); // Restart Folder watching if on - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); if (settings.EnableFolderWatching) { - await _libraryWatcher.RestartWatching(); + await libraryWatcher.RestartWatching(); } // Assign all the necessary users with this library side nav var userIds = admins.Select(u => u.Id).Append(UserId).ToList(); - var userNeedingNewLibrary = (await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.SideNavStreams)) + var userNeedingNewLibrary = (await unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.SideNavStreams)) .Where(u => userIds.Contains(u.Id)) .ToList(); foreach (var user in userNeedingNewLibrary) { user.CreateSideNavFromLibrary(library); - _unitOfWork.UserRepository.Update(user); + unitOfWork.UserRepository.Update(user); } - if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(UserId, "generic-library")); + if (!await unitOfWork.CommitAsync()) return BadRequest(await localizationService.Translate(UserId, "generic-library")); // I added this twice as some users were having issues where their new library wasn't added to the side nav. // I wasn't able to reproduce but could validate it didn't happen with this extra commit. (https://github.com/Kareadita/Kavita/issues/4248) - if (_unitOfWork.HasChanges()) + if (unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); } await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey); if (library.FolderWatching) { - await _libraryWatcher.RestartWatching(); + await libraryWatcher.RestartWatching(); } - BackgroundJob.Enqueue(() => _taskScheduler.ScanLibrary(library.Id, false)); - await _eventHub.SendMessageAsync(MessageFactory.LibraryModified, + BackgroundJob.Enqueue(() => taskScheduler.ScanLibrary(library.Id, false)); + await eventHub.SendMessageAsync(MessageFactory.LibraryModified, MessageFactory.LibraryModifiedEvent(library.Id, "create"), false); - await _eventHub.SendMessageAsync(MessageFactory.SideNavUpdate, + await eventHub.SendMessageAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(UserId), false); - return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtoByIdAsync(library.Id)); + return Ok(await unitOfWork.LibraryRepository.GetLibraryDtoByIdAsync(library.Id)); } /// @@ -170,8 +158,8 @@ public class LibraryController : BaseApiController /// /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpGet("list")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public ActionResult> GetDirectories(string? path) { if (string.IsNullOrEmpty(path)) @@ -183,21 +171,23 @@ public class LibraryController : BaseApiController })); } - if (!Directory.Exists(path)) return Ok(_directoryService.ListDirectory(Path.GetDirectoryName(path)!)); + if (path.Contains("..")) return BadRequest(); - return Ok(_directoryService.ListDirectory(path)); + if (!Directory.Exists(path)) return Ok(directoryService.ListDirectory(Path.GetDirectoryName(path)!)); + + return Ok(directoryService.ListDirectory(path)); } /// /// For each root, checks if there are any supported files at root to warn the user during library creation about an invalid setup /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("has-files-at-root")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public ActionResult> AnyFilesAtRoot(CheckForFilesInFolderRootsDto dto) { var foldersWithFilesAtRoot = dto.Roots - .Where(root => _directoryService + .Where(root => directoryService .GetFilesWithCertainExtensions(root, Parser.SupportedExtensions, SearchOption.TopDirectoryOnly) .Any()) .ToList(); @@ -210,16 +200,17 @@ public class LibraryController : BaseApiController /// /// If the user is not an admin, only id, type, and name will be returned /// + [HttpGet] + [LibraryAccess] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)] - [HttpGet] public async Task> GetLibrary(int libraryId) { if (User.IsInRole(PolicyConstants.AdminRole)) { - return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtoByIdAsync(libraryId)); + return Ok(await unitOfWork.LibraryRepository.GetLibraryDtoByIdAsync(libraryId)); } - return Ok(await _unitOfWork.LibraryRepository.GetLiteLibraryDtoByIdAsync(libraryId)); + return Ok(await unitOfWork.LibraryRepository.GetLiteLibraryDtoByIdAsync(libraryId)); } /// @@ -240,7 +231,7 @@ public class LibraryController : BaseApiController [HttpGet("user-libraries")] public async Task>> GetLibrariesForUser(int userId) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId); if (user == null || string.IsNullOrEmpty(user.UserName)) return BadRequest(); var ownLibraries = await GetLibrariesForUser(Username!); @@ -262,7 +253,7 @@ public class LibraryController : BaseApiController var result = await _libraryCacheProvider.GetAsync>(cacheKey); if (result.HasValue) return result.Value; - var ret = await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(username); + var ret = await unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(username); await _libraryCacheProvider.SetAsync(CacheKey, ret, TimeSpan.FromHours(24)); return ret; } @@ -272,13 +263,11 @@ public class LibraryController : BaseApiController /// /// /// + [LibraryAccess] [HttpGet("jump-bar")] - public async Task>> GetJumpBar(int libraryId) + public ActionResult> GetJumpBar(int libraryId) { - if (!await _unitOfWork.UserRepository.HasAccessToLibrary(libraryId, UserId)) - return BadRequest(await _localizationService.Translate(UserId, "no-library-access")); - - return Ok(_unitOfWork.LibraryRepository.GetJumpBarAsync(libraryId)); + return Ok(unitOfWork.LibraryRepository.GetJumpBarAsync(libraryId)); } /// @@ -286,25 +275,26 @@ public class LibraryController : BaseApiController /// /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("grant-access")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> UpdateUserLibraries(UpdateLibraryForUserDto updateLibraryForUserDto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(updateLibraryForUserDto.Username, AppUserIncludes.SideNavStreams); - if (user == null) return BadRequest(await _localizationService.Translate(UserId, "user-doesnt-exist")); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(updateLibraryForUserDto.Username, AppUserIncludes.SideNavStreams); + if (user == null) return BadRequest(await localizationService.Translate(UserId, "user-doesnt-exist")); var libraryString = string.Join(',', updateLibraryForUserDto.SelectedLibraries.Select(x => x.Name)); - _logger.LogInformation("Granting user {UserName} access to: {Libraries}", updateLibraryForUserDto.Username, libraryString); + logger.LogInformation("Granting user {UserId} access to: {Libraries}", user.Id, libraryString.Sanitize()); - var allLibraries = await _unitOfWork.LibraryRepository.GetLibrariesAsync(); + var allLibraries = await unitOfWork.LibraryRepository.GetLibrariesAsync(); foreach (var library in allLibraries) { - library.AppUsers ??= new List(); + library.AppUsers ??= []; + var libraryContainsUser = library.AppUsers.Any(u => u.UserName == user.UserName); var libraryIsSelected = updateLibraryForUserDto.SelectedLibraries.Any(l => l.Id == library.Id); + if (libraryContainsUser && !libraryIsSelected) { - // Remove library.AppUsers.Remove(user); user.RemoveSideNavFromLibrary(library); } @@ -315,25 +305,25 @@ public class LibraryController : BaseApiController } } - if (!_unitOfWork.HasChanges()) + if (!unitOfWork.HasChanges()) { - _logger.LogInformation("No changes for update library access"); - return Ok(_mapper.Map(user)); + logger.LogInformation("No changes for update library access"); + return Ok(mapper.Map(user)); } - if (await _unitOfWork.CommitAsync()) + if (await unitOfWork.CommitAsync()) { - _logger.LogInformation("Added: {SelectedLibraries} to {Username}",libraryString, updateLibraryForUserDto.Username); + logger.LogInformation("Added: {SelectedLibraries} to {UserId}", libraryString.Sanitize(), user.Id); // Bust cache await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey); - _unitOfWork.UserRepository.Update(user); + unitOfWork.UserRepository.Update(user); - return Ok(_mapper.Map(user)); + return Ok(mapper.Map(user)); } - return BadRequest(await _localizationService.Translate(UserId, "generic-library")); + return BadRequest(await localizationService.Translate(UserId, "generic-library")); } /// @@ -342,12 +332,12 @@ public class LibraryController : BaseApiController /// /// If true, will ignore any optimizations to avoid file I/O and will treat similar to a first scan /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("scan")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task Scan(int libraryId, bool force = false) { - if (libraryId <= 0) return BadRequest(await _localizationService.Translate(UserId, "greater-0", "libraryId")); - await _taskScheduler.ScanLibrary(libraryId, force); + if (libraryId <= 0) return BadRequest(await localizationService.Translate(UserId, "greater-0", "libraryId")); + await taskScheduler.ScanLibrary(libraryId, force); return Ok(); } @@ -355,13 +345,13 @@ public class LibraryController : BaseApiController /// Enqueues a bunch of library scans /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("scan-multiple")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task ScanMultiple(BulkActionDto dto) { foreach (var libraryId in dto.Ids) { - await _taskScheduler.ScanLibrary(libraryId, dto.Force ?? false); + await taskScheduler.ScanLibrary(libraryId, dto.Force ?? false); } return Ok(); @@ -372,19 +362,19 @@ public class LibraryController : BaseApiController /// /// If true, will ignore any optimizations to avoid file I/O and will treat similar to a first scan /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("scan-all")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public ActionResult ScanAll(bool force = false) { - _taskScheduler.ScanLibraries(force); + taskScheduler.ScanLibraries(force); return Ok(); } - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("refresh-metadata")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public ActionResult RefreshMetadata(int libraryId, bool force = true, bool forceColorscape = true) { - _taskScheduler.RefreshMetadata(libraryId, force, forceColorscape); + taskScheduler.RefreshMetadata(libraryId, force, forceColorscape); return Ok(); } @@ -394,7 +384,7 @@ public class LibraryController : BaseApiController { foreach (var libraryId in dto.Ids) { - _taskScheduler.RefreshMetadata(libraryId, dto.Force ?? false, forceColorscape); + taskScheduler.RefreshMetadata(libraryId, dto.Force ?? false, forceColorscape); } return Ok(); @@ -405,14 +395,14 @@ public class LibraryController : BaseApiController /// /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("copy-settings-from")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task CopySettingsFromLibraryToLibraries(CopySettingsFromLibraryDto dto) { - var sourceLibrary = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(dto.SourceLibraryId, LibraryIncludes.ExcludePatterns | LibraryIncludes.FileTypes); + var sourceLibrary = await unitOfWork.LibraryRepository.GetLibraryForIdAsync(dto.SourceLibraryId, LibraryIncludes.ExcludePatterns | LibraryIncludes.FileTypes); if (sourceLibrary == null) return BadRequest("SourceLibraryId must exist"); - var libraries = await _unitOfWork.LibraryRepository.GetLibraryForIdsAsync(dto.TargetLibraryIds, LibraryIncludes.ExcludePatterns | LibraryIncludes.FileTypes | LibraryIncludes.Folders); + var libraries = await unitOfWork.LibraryRepository.GetLibraryForIdsAsync(dto.TargetLibraryIds, LibraryIncludes.ExcludePatterns | LibraryIncludes.FileTypes | LibraryIncludes.Folders); foreach (var targetLibrary in libraries) { UpdateLibrarySettings(new UpdateLibraryDto() @@ -432,11 +422,11 @@ public class LibraryController : BaseApiController }, targetLibrary, dto.IncludeType); } - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); if (sourceLibrary.FolderWatching) { - BackgroundJob.Enqueue(() => _libraryWatcher.RestartWatching()); + BackgroundJob.Enqueue(() => libraryWatcher.RestartWatching()); } return Ok(); @@ -451,28 +441,28 @@ public class LibraryController : BaseApiController [HttpPost("scan-folder")] public async Task ScanFolder(ScanFolderDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByAuthKey(dto.ApiKey); + var user = await unitOfWork.UserRepository.GetUserByAuthKey(dto.ApiKey); if (user == null) return Unauthorized(); // Validate user has Admin privileges - var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user); + var isAdmin = await unitOfWork.UserRepository.IsUserAdminAsync(user); if (!isAdmin) return BadRequest("API key must belong to an admin"); if (dto.FolderPath.Contains("..")) { - return BadRequest(await _localizationService.Translate(UserId, "invalid-path")); + return BadRequest(await localizationService.Translate(UserId, "invalid-path")); } dto.FolderPath = Parser.NormalizePath(dto.FolderPath); - var libraryFolder = (await _unitOfWork.LibraryRepository.GetLibraryDtosAsync()) + var libraryFolder = (await unitOfWork.LibraryRepository.GetLibraryDtosAsync()) .SelectMany(l => l.Folders) .Distinct() .Select(Parser.NormalizePath); - var seriesFolder = _directoryService.FindHighestDirectoriesFromFiles(libraryFolder, [dto.FolderPath]); + var seriesFolder = directoryService.FindHighestDirectoriesFromFiles(libraryFolder, [dto.FolderPath]); - _taskScheduler.ScanFolder(seriesFolder.Keys.Count == 1 ? seriesFolder.Keys.First() : dto.FolderPath, dto.AbortOnNoSeriesMatch); + taskScheduler.ScanFolder(seriesFolder.Keys.Count == 1 ? seriesFolder.Keys.First() : dto.FolderPath, dto.AbortOnNoSeriesMatch); return Ok(); } @@ -483,11 +473,11 @@ public class LibraryController : BaseApiController /// This does not touch any files /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpDelete("delete")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> DeleteLibrary(int libraryId) { - _logger.LogInformation("Library {LibraryId} is being deleted by {UserName}", libraryId, Username!); + logger.LogInformation("Library {LibraryId} is being deleted by {UserName}", libraryId, Username!); try { @@ -495,7 +485,7 @@ public class LibraryController : BaseApiController if (result) { // Inform the user's side nav to remove it if needed - await _eventHub.SendMessageAsync(MessageFactory.SideNavUpdate, + await eventHub.SendMessageAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(UserId), false); } return Ok(result); @@ -512,14 +502,18 @@ public class LibraryController : BaseApiController /// This does not touch any files /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpDelete("delete-multiple")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> DeleteMultipleLibraries([FromQuery] List libraryIds) { var username = Username!; - _logger.LogInformation("Libraries {LibraryIds} are being deleted by {UserName}", libraryIds, username); - foreach (var libraryId in libraryIds) + var allLibraries = await unitOfWork.LibraryRepository.GetLibrariesAsync(); + var toDelete = allLibraries.Where(l => libraryIds.Contains(l.Id)).Select(l => l.Id).ToList(); + + logger.LogInformation("Libraries {LibraryIds} are being deleted by {UserName}", toDelete, username); + + foreach (var libraryId in toDelete) { try { @@ -536,79 +530,79 @@ public class LibraryController : BaseApiController private async Task DeleteLibrary(int libraryId, int userId) { - var series = await _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(libraryId); + var series = await unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(libraryId); var seriesIds = series.Select(x => x.Id).ToArray(); var chapterIds = - await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(seriesIds); + await unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(seriesIds); try { if (TaskScheduler.HasScanTaskRunningForLibrary(libraryId)) { - _logger.LogInformation("User is attempting to delete a library while a scan is in progress"); - throw new KavitaException(await _localizationService.Translate(userId, "delete-library-while-scan")); + logger.LogInformation("User is attempting to delete a library while a scan is in progress"); + throw new KavitaException(await localizationService.Translate(userId, "delete-library-while-scan")); } - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); + var library = await unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); if (library == null) { - throw new KavitaException(await _localizationService.Translate(userId, "library-doesnt-exist")); + throw new KavitaException(await localizationService.Translate(userId, "library-doesnt-exist")); } // Due to a bad schema that I can't figure out how to fix, we need to erase all RelatedSeries before we delete the library // Aka SeriesRelation has an invalid foreign key - foreach (var s in await _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(library.Id, SeriesIncludes.Related)) + foreach (var s in await unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(library.Id, SeriesIncludes.Related)) { s.Relations = new List(); - _unitOfWork.SeriesRepository.Update(s); + unitOfWork.SeriesRepository.Update(s); } - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); - _unitOfWork.LibraryRepository.Delete(library); + unitOfWork.LibraryRepository.Delete(library); - var streams = await _unitOfWork.UserRepository.GetSideNavStreamsByLibraryId(library.Id); - _unitOfWork.UserRepository.Delete(streams); + var streams = await unitOfWork.UserRepository.GetSideNavStreamsByLibraryId(library.Id); + unitOfWork.UserRepository.Delete(streams); - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey); - await _eventHub.SendMessageAsync(MessageFactory.SideNavUpdate, + await eventHub.SendMessageAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId), false); if (chapterIds.Any()) { - await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters(); - await _unitOfWork.CommitAsync(); - _taskScheduler.CleanupChapters(chapterIds); + await unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters(); + await unitOfWork.CommitAsync(); + taskScheduler.CleanupChapters(chapterIds); } - BackgroundJob.Enqueue(() => _libraryWatcher.RestartWatching()); + BackgroundJob.Enqueue(() => libraryWatcher.RestartWatching()); foreach (var seriesId in seriesIds) { - await _eventHub.SendMessageAsync(MessageFactory.SeriesRemoved, + await eventHub.SendMessageAsync(MessageFactory.SeriesRemoved, MessageFactory.SeriesRemovedEvent(seriesId, string.Empty, libraryId), false); } - await _eventHub.SendMessageAsync(MessageFactory.LibraryModified, + await eventHub.SendMessageAsync(MessageFactory.LibraryModified, MessageFactory.LibraryModifiedEvent(libraryId, "delete"), false); - var userPreferences = await _unitOfWork.DataContext.AppUserPreferences.ToListAsync(); + var userPreferences = await unitOfWork.DataContext.AppUserPreferences.ToListAsync(); foreach (var userPreference in userPreferences) { userPreference.SocialPreferences.SocialLibraries = userPreference.SocialPreferences.SocialLibraries .Where(l => l != libraryId).ToList(); } - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); return true; } catch (Exception ex) { - _logger.LogError(ex, "There was a critical issue. Please try again"); - await _unitOfWork.RollbackAsync(); + logger.LogError(ex, "There was a critical issue. Please try again"); + await unitOfWork.RollbackAsync(); return false; } } @@ -618,12 +612,12 @@ public class LibraryController : BaseApiController /// /// If empty or null, will return true as that is invalid /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpGet("name-exists")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> IsLibraryNameValid(string name) { if (string.IsNullOrWhiteSpace(name)) return Ok(true); - return Ok(await _unitOfWork.LibraryRepository.LibraryExists(name.Trim())); + return Ok(await unitOfWork.LibraryRepository.LibraryExists(name.Trim())); } /// @@ -632,17 +626,17 @@ public class LibraryController : BaseApiController /// Any folder or type change will invoke a scan. /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("update")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task UpdateLibrary(UpdateLibraryDto dto) { var userId = UserId; - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(dto.Id, LibraryIncludes.Folders | LibraryIncludes.FileTypes | LibraryIncludes.ExcludePatterns); - if (library == null) return BadRequest(await _localizationService.Translate(userId, "library-doesnt-exist")); + var library = await unitOfWork.LibraryRepository.GetLibraryForIdAsync(dto.Id, LibraryIncludes.Folders | LibraryIncludes.FileTypes | LibraryIncludes.ExcludePatterns); + if (library == null) return BadRequest(await localizationService.Translate(userId, "library-doesnt-exist")); var newName = dto.Name.Trim(); - if (await _unitOfWork.LibraryRepository.LibraryExists(newName) && !library.Name.Equals(newName)) - return BadRequest(await _localizationService.Translate(userId, "library-name-exists")); + if (await unitOfWork.LibraryRepository.LibraryExists(newName) && !library.Name.Equals(newName)) + return BadRequest(await localizationService.Translate(userId, "library-name-exists")); var originalFoldersCount = library.Folders.Count; @@ -653,27 +647,27 @@ public class LibraryController : BaseApiController var folderWatchingUpdate = library.FolderWatching != dto.FolderWatching; UpdateLibrarySettings(dto, library); - if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(userId, "generic-library-update")); + if (!await unitOfWork.CommitAsync()) return BadRequest(await localizationService.Translate(userId, "generic-library-update")); if (folderWatchingUpdate || originalFoldersCount != dto.Folders.Count() || typeUpdate) { - BackgroundJob.Enqueue(() => _libraryWatcher.RestartWatching()); + BackgroundJob.Enqueue(() => libraryWatcher.RestartWatching()); } if (originalFoldersCount != dto.Folders.Count() || typeUpdate) { - await _taskScheduler.ScanLibrary(library.Id); + await taskScheduler.ScanLibrary(library.Id); } - await _eventHub.SendMessageAsync(MessageFactory.LibraryModified, + await eventHub.SendMessageAsync(MessageFactory.LibraryModified, MessageFactory.LibraryModifiedEvent(library.Id, "update"), false); - await _eventHub.SendMessageAsync(MessageFactory.SideNavUpdate, + await eventHub.SendMessageAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId), false); await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey); - return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtoByIdAsync(library.Id)); + return Ok(await unitOfWork.LibraryRepository.GetLibraryDtoByIdAsync(library.Id)); } @@ -711,12 +705,12 @@ public class LibraryController : BaseApiController // Override Scrobbling for Comic libraries since there are no providers to scrobble to if (library.Type is LibraryType.Comic or LibraryType.ComicVine) { - _logger.LogInformation("Overrode Library {Name} to disable scrobbling since there are no providers for Comics", dto.Name.Replace(Environment.NewLine, string.Empty)); + logger.LogInformation("Overrode Library {Name} to disable scrobbling since there are no providers for Comics", dto.Name.Replace(Environment.NewLine, string.Empty)); library.AllowScrobbling = false; } - _unitOfWork.LibraryRepository.Update(library); + unitOfWork.LibraryRepository.Update(library); } /// @@ -724,9 +718,10 @@ public class LibraryController : BaseApiController /// /// /// + [LibraryAccess] [HttpGet("type")] public async Task> GetLibraryType(int libraryId) { - return Ok(await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(libraryId)); + return Ok(await unitOfWork.LibraryRepository.GetLibraryTypeAsync(libraryId)); } } diff --git a/API/Controllers/LicenseController.cs b/Kavita.Server/Controllers/LicenseController.cs similarity index 92% rename from API/Controllers/LicenseController.cs rename to Kavita.Server/Controllers/LicenseController.cs index 9e946b222..8e30330e7 100644 --- a/API/Controllers/LicenseController.cs +++ b/Kavita.Server/Controllers/LicenseController.cs @@ -1,20 +1,19 @@ using System; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.KavitaPlus.License; -using API.Entities.Enums; -using API.Services; -using API.Services.Plus; using EasyCaching.Core; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.KavitaPlus.License; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Plus; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using TaskScheduler = API.Services.TaskScheduler; +using TaskScheduler = Kavita.Services.TaskScheduler; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; public class LicenseController( IUnitOfWork unitOfWork, @@ -50,8 +49,8 @@ public class LicenseController( /// Has any license registered with the instance. Does not validate against Kavita+ API /// /// - [Authorize(PolicyGroups.AdminPolicy)] [HttpGet("has-license")] + [Authorize(PolicyGroups.AdminPolicy)] public async Task> HasLicense() { return Ok(!string.IsNullOrEmpty( @@ -63,8 +62,8 @@ public class LicenseController( /// /// Force checking the API and skip the 8-hour cache /// - [Authorize(PolicyGroups.AdminPolicy)] [HttpGet("info")] + [Authorize(PolicyGroups.AdminPolicy)] public async Task> GetLicenseInfo(bool forceCheck = false) { try @@ -81,8 +80,8 @@ public class LicenseController( /// Remove the Kavita+ License on the Server /// /// - [Authorize(PolicyGroups.AdminPolicy)] [HttpDelete] + [Authorize(PolicyGroups.AdminPolicy)] public async Task RemoveLicense() { logger.LogInformation("Removing license on file for Server"); @@ -97,8 +96,8 @@ public class LicenseController( } - [Authorize(PolicyGroups.AdminPolicy)] [HttpPost("reset")] + [Authorize(PolicyGroups.AdminPolicy)] public async Task ResetLicense(UpdateLicenseDto dto) { logger.LogInformation("Resetting license on file for Server"); @@ -126,8 +125,8 @@ public class LicenseController( /// /// Caches the result /// - [Authorize(PolicyGroups.AdminPolicy)] [HttpPost] + [Authorize(PolicyGroups.AdminPolicy)] public async Task UpdateLicense(UpdateLicenseDto dto) { try diff --git a/API/Controllers/LocaleController.cs b/Kavita.Server/Controllers/LocaleController.cs similarity index 58% rename from API/Controllers/LocaleController.cs rename to Kavita.Server/Controllers/LocaleController.cs index 44713e35d..d716d1681 100644 --- a/API/Controllers/LocaleController.cs +++ b/Kavita.Server/Controllers/LocaleController.cs @@ -2,38 +2,32 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.DTOs; -using API.Services; using EasyCaching.Core; +using Kavita.API.Services; using Kavita.Common.EnvironmentInfo; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; +namespace Kavita.Server.Controllers; -#nullable enable - -public class LocaleController : BaseApiController +public class LocaleController( + ILocalizationService localizationService, + IEasyCachingProviderFactory cachingProviderFactory) + : BaseApiController { - private readonly ILocalizationService _localizationService; - private readonly IEasyCachingProvider _localeCacheProvider; + private readonly IEasyCachingProvider _localeCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.LocaleOptions); private static readonly string CacheKey = "locales_" + BuildInfo.Version; - public LocaleController(ILocalizationService localizationService, IEasyCachingProviderFactory cachingProviderFactory) - { - _localizationService = localizationService; - _localeCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.LocaleOptions); - } - /// /// Returns all applicable locales on the server /// /// This can be cached as it will not change per version. /// - [AllowAnonymous] [HttpGet] + [AllowAnonymous] public async Task>> GetAllLocales() { var result = await _localeCacheProvider.GetAsync>(CacheKey); @@ -42,7 +36,7 @@ public class LocaleController : BaseApiController return Ok(result.Value); } - var ret = _localizationService.GetLocales().Where(l => l.TranslationCompletion > 0f); + var ret = localizationService.GetLocales().Where(l => l.TranslationCompletion > 0f); await _localeCacheProvider.SetAsync(CacheKey, ret, TimeSpan.FromDays(1)); return Ok(ret); diff --git a/API/Controllers/ManageController.cs b/Kavita.Server/Controllers/ManageController.cs similarity index 50% rename from API/Controllers/ManageController.cs rename to Kavita.Server/Controllers/ManageController.cs index 0469f5a32..596d97478 100644 --- a/API/Controllers/ManageController.cs +++ b/Kavita.Server/Controllers/ManageController.cs @@ -1,47 +1,34 @@ -#nullable enable -using System; -using System.Collections.Generic; +using System; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs; -using API.DTOs.KavitaPlus.Manage; -using API.Extensions; -using API.Helpers; -using API.Services.Plus; +using Kavita.API.Database; +using Kavita.Common.Helpers; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.KavitaPlus.Manage; +using Kavita.Server.Attributes; +using Kavita.Server.Extensions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; +namespace Kavita.Server.Controllers; /// /// All things centered around Managing the Kavita instance, that isn't aligned with an entity /// [Authorize(PolicyGroups.AdminPolicy)] -public class ManageController : BaseApiController +public class ManageController(IUnitOfWork unitOfWork) : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly ILicenseService _licenseService; - - public ManageController(IUnitOfWork unitOfWork, ILicenseService licenseService) - { - _unitOfWork = unitOfWork; - _licenseService = licenseService; - } - /// /// Returns a list of all Series that is Kavita+ applicable to metadata match and the status of it /// /// + [KPlus] [Authorize(PolicyGroups.AdminPolicy)] [HttpPost("series-metadata")] public async Task>> SeriesMetadata(ManageMatchFilterDto filter, [FromQuery] UserParams? userParams) { - //if (!await _licenseService.HasActiveLicense()) return Ok(Array.Empty()); - userParams ??= UserParams.Default; - var res = await _unitOfWork.ExternalSeriesMetadataRepository.GetAllSeries(filter, userParams); + var res = await unitOfWork.ExternalSeriesMetadataRepository.GetAllSeries(filter, userParams); Response.AddPaginationHeader(res); return Ok(res); diff --git a/API/Controllers/MetadataController.cs b/Kavita.Server/Controllers/MetadataController.cs similarity index 93% rename from API/Controllers/MetadataController.cs rename to Kavita.Server/Controllers/MetadataController.cs index 795a9a02e..d5246584a 100644 --- a/API/Controllers/MetadataController.cs +++ b/Kavita.Server/Controllers/MetadataController.cs @@ -3,24 +3,23 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Filtering; -using API.DTOs.Metadata; -using API.DTOs.Metadata.Browse; -using API.DTOs.Person; -using API.DTOs.SeriesDetail; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers; -using API.Services; -using API.Services.Plus; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services.Plus; using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Metadata.Browse; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.Entities.Enums; +using Kavita.Server.Extensions; +using Kavita.Services.Helpers; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; -#nullable enable +namespace Kavita.Server.Controllers; public class MetadataController(IUnitOfWork unitOfWork, IExternalMetadataService metadataService) : BaseApiController { @@ -126,8 +125,8 @@ public class MetadataController(IUnitOfWork unitOfWork, IExternalMetadataService /// String separated libraryIds or null for all ratings /// This API is cached for 1 hour, varying by libraryIds /// - [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = ["libraryIds"])] [HttpGet("age-ratings")] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = ["libraryIds"])] public async Task>> GetAllAgeRatings(string? libraryIds) { var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(); @@ -149,8 +148,8 @@ public class MetadataController(IUnitOfWork unitOfWork, IExternalMetadataService /// String separated libraryIds or null for all publication status /// This API is cached for 1 hour, varying by libraryIds /// - [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = ["libraryIds"])] [HttpGet("publication-status")] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute, VaryByQueryKeys = ["libraryIds"])] public ActionResult> GetAllPublicationStatus(string? libraryIds) { var ids = libraryIds?.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToList(); @@ -236,7 +235,6 @@ public class MetadataController(IUnitOfWork unitOfWork, IExternalMetadataService private async Task PrepareSeriesDetail(List userReviews, SeriesDetailPlusDto? ret) { - var isAdmin = User.IsInRole(PolicyConstants.AdminRole); var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId)!; if (ret != null) @@ -245,13 +243,17 @@ public class MetadataController(IUnitOfWork unitOfWork, IExternalMetadataService ret.Reviews = userReviews; } - if (!isAdmin && ret?.Recommendations != null && user != null) + if (ret?.Recommendations != null && user != null) { - // Re-obtain owned series and take into account age restriction + // Re-obtain owned series and take into account age restriction and include series progress var seriesIds = ret.Recommendations.OwnedSeries.Select(s => s.Id); ret.Recommendations.OwnedSeries = await unitOfWork.SeriesRepository.GetSeriesDtoByIdsAsync(seriesIds, user); - ret.Recommendations.ExternalSeries = []; + + if (!User.IsInRole(PolicyConstants.AdminRole)) + { + ret.Recommendations.ExternalSeries = []; + } } if (ret?.Recommendations != null && user != null) diff --git a/API/Controllers/OPDSController.cs b/Kavita.Server/Controllers/OPDSController.cs similarity index 71% rename from API/Controllers/OPDSController.cs rename to Kavita.Server/Controllers/OPDSController.cs index 1e2b8bba2..ed69c1472 100644 --- a/API/Controllers/OPDSController.cs +++ b/Kavita.Server/Controllers/OPDSController.cs @@ -2,52 +2,36 @@ using System; using System.IO; using System.Threading.Tasks; using System.Xml.Serialization; -using API.Constants; -using API.Data; -using API.DTOs.OPDS; -using API.DTOs.OPDS.Requests; -using API.DTOs.Progress; -using API.Entities.Enums; -using API.Exceptions; -using API.Services; -using API.Services.Reading; +using Kavita.API.Database; +using Kavita.API.Errors; +using Kavita.API.Services; +using Kavita.API.Services.Reading; using Kavita.Common; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.OPDS; +using Kavita.Models.DTOs.OPDS.Requests; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities.Enums; +using Kavita.Server.Attributes; +using Kavita.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using MimeTypes; -namespace API.Controllers; -#nullable enable +namespace Kavita.Server.Controllers; [Authorize] -public class OpdsController : BaseApiController +public class OpdsController( + IUnitOfWork unitOfWork, + IDownloadService downloadService, + IDirectoryService directoryService, + ICacheService cacheService, + IReaderService readerService, + ILocalizationService localizationService, + IOpdsService opdsService) + : BaseApiController { - private readonly IOpdsService _opdsService; - private readonly IUnitOfWork _unitOfWork; - private readonly IDownloadService _downloadService; - private readonly IDirectoryService _directoryService; - private readonly ICacheService _cacheService; - private readonly IReaderService _readerService; - private readonly IAccountService _accountService; - private readonly ILocalizationService _localizationService; - private readonly XmlSerializer _xmlOpenSearchSerializer; - - public OpdsController(IUnitOfWork unitOfWork, IDownloadService downloadService, - IDirectoryService directoryService, ICacheService cacheService, - IReaderService readerService, IAccountService accountService, - ILocalizationService localizationService, IOpdsService opdsService) - { - _unitOfWork = unitOfWork; - _downloadService = downloadService; - _directoryService = directoryService; - _cacheService = cacheService; - _readerService = readerService; - _accountService = accountService; - _localizationService = localizationService; - _opdsService = opdsService; - - _xmlOpenSearchSerializer = new XmlSerializer(typeof(OpenSearchDescription)); - } + private readonly XmlSerializer _xmlOpenSearchSerializer = new(typeof(OpenSearchDescription)); /// @@ -62,22 +46,22 @@ public class OpdsController : BaseApiController { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetCatalogue(new OpdsCatalogueRequest + var feed = await opdsService.GetCatalogue(new OpdsCatalogueRequest { ApiKey = apiKey, Prefix = prefix, BaseUrl = baseUrl, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), UserId = UserId }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } private async Task> GetPrefix() { - var baseUrl = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BaseUrl)).Value; + var baseUrl = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BaseUrl)).Value; var prefix = OpdsService.DefaultApiPrefix; if (!Configuration.DefaultBaseUrl.Equals(baseUrl, StringComparison.InvariantCultureIgnoreCase)) { @@ -92,26 +76,26 @@ public class OpdsController : BaseApiController /// Get the User's Smart Filter series - Supports Pagination /// /// - [HttpGet("{apiKey}/smart-filters/{filterId}")] [Produces("application/xml")] + [HttpGet("{apiKey}/smart-filters/{filterId}")] public async Task GetSmartFilter(string apiKey, int filterId, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { var userId = UserId; var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetSeriesFromSmartFilter(new OpdsItemsFromEntityIdRequest() + var feed = await opdsService.GetSeriesFromSmartFilter(new OpdsItemsFromEntityIdRequest() { ApiKey = apiKey, Prefix = prefix, BaseUrl = baseUrl, EntityId = filterId, UserId = userId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), PageNumber = pageNumber }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } /// @@ -120,8 +104,8 @@ public class OpdsController : BaseApiController /// /// /// - [HttpGet("{apiKey}/smart-filters")] [Produces("application/xml")] + [HttpGet("{apiKey}/smart-filters")] public async Task GetSmartFilters(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try @@ -129,17 +113,17 @@ public class OpdsController : BaseApiController var userId = UserId; var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetSmartFilters(new OpdsPaginatedCatalogueRequest() + var feed = await opdsService.GetSmartFilters(new OpdsPaginatedCatalogueRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = userId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, PageNumber = pageNumber }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -161,17 +145,17 @@ public class OpdsController : BaseApiController { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetLibraries(new OpdsPaginatedCatalogueRequest() + var feed = await opdsService.GetLibraries(new OpdsPaginatedCatalogueRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, PageNumber = pageNumber }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -185,25 +169,25 @@ public class OpdsController : BaseApiController /// /// /// - [HttpGet("{apiKey}/want-to-read")] [Produces("application/xml")] + [HttpGet("{apiKey}/want-to-read")] public async Task GetWantToRead(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetWantToRead(new OpdsPaginatedCatalogueRequest() + var feed = await opdsService.GetWantToRead(new OpdsPaginatedCatalogueRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, PageNumber = pageNumber }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -217,25 +201,25 @@ public class OpdsController : BaseApiController /// /// /// - [HttpGet("{apiKey}/collections")] [Produces("application/xml")] + [HttpGet("{apiKey}/collections")] public async Task GetCollections(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetCollections(new OpdsPaginatedCatalogueRequest() + var feed = await opdsService.GetCollections(new OpdsPaginatedCatalogueRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, PageNumber = pageNumber }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -250,26 +234,26 @@ public class OpdsController : BaseApiController /// /// /// - [HttpGet("{apiKey}/collections/{collectionId}")] [Produces("application/xml")] + [HttpGet("{apiKey}/collections/{collectionId}")] public async Task GetCollection(int collectionId, string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetSeriesFromCollection(new OpdsItemsFromEntityIdRequest() + var feed = await opdsService.GetSeriesFromCollection(new OpdsItemsFromEntityIdRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, PageNumber = pageNumber, EntityId = collectionId }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -283,25 +267,25 @@ public class OpdsController : BaseApiController /// /// /// - [HttpGet("{apiKey}/reading-list")] [Produces("application/xml")] + [HttpGet("{apiKey}/reading-list")] public async Task GetReadingLists(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetReadingLists(new OpdsPaginatedCatalogueRequest() + var feed = await opdsService.GetReadingLists(new OpdsPaginatedCatalogueRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, PageNumber = pageNumber }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -316,26 +300,26 @@ public class OpdsController : BaseApiController /// /// /// - [HttpGet("{apiKey}/reading-list/{readingListId}")] [Produces("application/xml")] + [HttpGet("{apiKey}/reading-list/{readingListId}")] public async Task GetReadingListItems(int readingListId, string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetReadingListItems(new OpdsItemsFromEntityIdRequest() + var feed = await opdsService.GetReadingListItems(new OpdsItemsFromEntityIdRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, PageNumber = pageNumber, EntityId = readingListId }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -351,26 +335,26 @@ public class OpdsController : BaseApiController /// /// /// - [HttpGet("{apiKey}/libraries/{libraryId}")] [Produces("application/xml")] + [HttpGet("{apiKey}/libraries/{libraryId}")] public async Task GetSeriesForLibrary(int libraryId, string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetSeriesFromLibrary(new OpdsItemsFromEntityIdRequest() + var feed = await opdsService.GetSeriesFromLibrary(new OpdsItemsFromEntityIdRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, PageNumber = pageNumber, EntityId = libraryId }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -384,24 +368,24 @@ public class OpdsController : BaseApiController /// /// /// - [HttpGet("{apiKey}/recently-added")] [Produces("application/xml")] + [HttpGet("{apiKey}/recently-added")] public async Task GetRecentlyAdded(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetRecentlyAdded(new OpdsPaginatedCatalogueRequest() + var feed = await opdsService.GetRecentlyAdded(new OpdsPaginatedCatalogueRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, PageNumber = pageNumber, }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -416,25 +400,25 @@ public class OpdsController : BaseApiController /// /// /// - [HttpGet("{apiKey}/more-in-genre")] [Produces("application/xml")] + [HttpGet("{apiKey}/more-in-genre")] public async Task GetMoreInGenre(string apiKey, [FromQuery] int genreId, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetMoreInGenre(new OpdsItemsFromEntityIdRequest() + var feed = await opdsService.GetMoreInGenre(new OpdsItemsFromEntityIdRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, PageNumber = pageNumber, EntityId = genreId }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -448,24 +432,24 @@ public class OpdsController : BaseApiController /// /// /// - [HttpGet("{apiKey}/recently-updated")] [Produces("application/xml")] + [HttpGet("{apiKey}/recently-updated")] public async Task GetRecentlyUpdated(string apiKey, [FromQuery] int pageNumber = OpdsService.FirstPageNumber) { try { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetRecentlyUpdated(new OpdsPaginatedCatalogueRequest() + var feed = await opdsService.GetRecentlyUpdated(new OpdsPaginatedCatalogueRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, PageNumber = pageNumber, }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -486,17 +470,17 @@ public class OpdsController : BaseApiController try { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetOnDeck(new OpdsPaginatedCatalogueRequest() + var feed = await opdsService.GetOnDeck(new OpdsPaginatedCatalogueRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, PageNumber = pageNumber, }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -517,17 +501,17 @@ public class OpdsController : BaseApiController try { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.Search(new OpdsSearchRequest() + var feed = await opdsService.Search(new OpdsSearchRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, Query = query, }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -544,8 +528,8 @@ public class OpdsController : BaseApiController var feed = new OpenSearchDescription() { - ShortName = await _localizationService.Translate(userId, "search"), - Description = await _localizationService.Translate(userId, "search-description"), + ShortName = await localizationService.Translate(userId, "search"), + Description = await localizationService.Translate(userId, "search-description"), Url = new SearchLink() { Type = FeedLinkType.AtomAcquisition, @@ -565,6 +549,7 @@ public class OpdsController : BaseApiController /// /// /// + [SeriesAccess] [HttpGet("{apiKey}/series/{seriesId}")] [Produces("application/xml")] public async Task GetSeriesDetail(string apiKey, int seriesId) @@ -573,17 +558,17 @@ public class OpdsController : BaseApiController { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetSeriesDetail(new OpdsItemsFromEntityIdRequest() + var feed = await opdsService.GetSeriesDetail(new OpdsItemsFromEntityIdRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, EntityId = seriesId }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -598,26 +583,27 @@ public class OpdsController : BaseApiController /// /// /// - [HttpGet("{apiKey}/series/{seriesId}/volume/{volumeId}")] + [VolumeAccess] [Produces("application/xml")] + [HttpGet("{apiKey}/series/{seriesId}/volume/{volumeId}")] public async Task GetVolume(string apiKey, int seriesId, int volumeId) { try { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetItemsFromVolume(new OpdsItemsFromCompoundEntityIdsRequest() + var feed = await opdsService.GetItemsFromVolume(new OpdsItemsFromCompoundEntityIdsRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, SeriesId = seriesId, VolumeId = volumeId }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -633,27 +619,28 @@ public class OpdsController : BaseApiController /// /// /// - [HttpGet("{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}")] + [ChapterAccess] [Produces("application/xml")] + [HttpGet("{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}")] public async Task GetChapter(string apiKey, int seriesId, int volumeId, int chapterId) { try { var (baseUrl, prefix) = await GetPrefix(); - var feed = await _opdsService.GetItemsFromChapter(new OpdsItemsFromCompoundEntityIdsRequest() + var feed = await opdsService.GetItemsFromChapter(new OpdsItemsFromCompoundEntityIdsRequest() { BaseUrl = baseUrl, Prefix = prefix, UserId = UserId, - Preferences = await _unitOfWork.UserRepository.GetOpdsPreferences(UserId), + Preferences = await unitOfWork.UserRepository.GetOpdsPreferences(UserId), ApiKey = apiKey, SeriesId = seriesId, VolumeId = volumeId, ChapterId = chapterId }); - return CreateXmlResult(_opdsService.SerializeXml(feed)); + return CreateXmlResult(opdsService.SerializeXml(feed)); } catch (OpdsException ex) { @@ -670,18 +657,13 @@ public class OpdsController : BaseApiController /// /// Not used. Only for Chunky to allow download links /// + [ChapterAccess] + [Authorize(PolicyConstants.DownloadRole)] [HttpGet("{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}/download/{filename}")] public async Task DownloadFile(string apiKey, int seriesId, int volumeId, int chapterId, string filename) { - var userId = UserId; - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - if (!await _accountService.HasDownloadPermission(user)) - { - return Forbid(await _localizationService.Translate(userId, "download-not-allowed")); - } - - var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId); - var (zipFile, contentType, fileDownloadName) = _downloadService.GetFirstFileDownload(files); + var files = await unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId); + var (zipFile, contentType, fileDownloadName) = downloadService.GetFirstFileDownload(files); return PhysicalFile(zipFile, contentType, fileDownloadName, true); } @@ -707,22 +689,23 @@ public class OpdsController : BaseApiController /// /// Optional parameter. Can pass false and progress saving will be suppressed /// + [ChapterAccess] [HttpGet("{apiKey}/image")] public async Task GetPageStreamedImage(string apiKey, [FromQuery] int libraryId, [FromQuery] int seriesId, [FromQuery] int volumeId,[FromQuery] int chapterId, [FromQuery] int pageNumber, [FromQuery] bool saveProgress = true) { var userId = UserId; - if (pageNumber < 0) return BadRequest(await _localizationService.Translate(userId, "greater-0", "Page")); - var chapter = await _cacheService.Ensure(chapterId, true); - if (chapter == null) return BadRequest(await _localizationService.Translate(userId, "cache-file-find")); + if (pageNumber < 0) return BadRequest(await localizationService.Translate(userId, "greater-0", "Page")); + var chapter = await cacheService.Ensure(chapterId, true); + if (chapter == null) return BadRequest(await localizationService.Translate(userId, "cache-file-find")); try { - var path = _cacheService.GetCachedPagePath(chapter.Id, pageNumber); + var path = cacheService.GetCachedPagePath(chapter.Id, pageNumber); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) - return BadRequest(await _localizationService.Translate(userId, "no-image-for-page", pageNumber)); + return BadRequest(await localizationService.Translate(userId, "no-image-for-page", pageNumber)); - var content = await _directoryService.ReadFileAsync(path); + var content = await directoryService.ReadFileAsync(path); var format = Path.GetExtension(path); // Save progress for the user (except Panels, they will use a direct connection) @@ -735,14 +718,14 @@ public class OpdsController : BaseApiController var koreaderOffset = 0; if (userAgent.StartsWith("Koreader", StringComparison.InvariantCultureIgnoreCase)) { - var totalPages = await _unitOfWork.ChapterRepository.GetChapterTotalPagesAsync(chapterId); + var totalPages = await unitOfWork.ChapterRepository.GetChapterTotalPagesAsync(chapterId); if (totalPages - pageNumber < 2) { koreaderOffset = 1; } } - await _readerService.SaveReadingProgress(new ProgressDto() + await readerService.SaveReadingProgress(new ProgressDto() { ChapterId = chapterId, PageNum = pageNumber + koreaderOffset, @@ -756,7 +739,7 @@ public class OpdsController : BaseApiController } catch (Exception) { - _cacheService.CleanupChapters([chapterId]); + cacheService.CleanupChapters([chapterId]); throw; } } @@ -765,11 +748,11 @@ public class OpdsController : BaseApiController [ResponseCache(CacheProfileName = ResponseCacheProfiles.Month)] public async Task GetFavicon(string apiKey) { - var files = _directoryService.GetFilesWithExtension(Path.Join(Directory.GetCurrentDirectory(), ".."), @"\.ico"); - if (files.Length == 0) return BadRequest(await _localizationService.Translate(UserId, "favicon-doesnt-exist")); + var files = directoryService.GetFilesWithExtension(Path.Join(Directory.GetCurrentDirectory(), ".."), @"\.ico"); + if (files.Length == 0) return BadRequest(await localizationService.Translate(UserId, "favicon-doesnt-exist")); var path = files[0]; - var content = await _directoryService.ReadFileAsync(path); + var content = await directoryService.ReadFileAsync(path); var format = Path.GetExtension(path); return File(content, MimeTypeMap.GetMimeType(format)); diff --git a/API/Controllers/OidcController.cs b/Kavita.Server/Controllers/OidcController.cs similarity index 83% rename from API/Controllers/OidcController.cs rename to Kavita.Server/Controllers/OidcController.cs index 87c3637bb..2f15746bd 100644 --- a/API/Controllers/OidcController.cs +++ b/Kavita.Server/Controllers/OidcController.cs @@ -1,28 +1,26 @@ -#nullable enable -using System.Threading.Tasks; -using API.Extensions; -using API.Middleware; -using API.Services; +using System.Threading.Tasks; +using Kavita.API.Attributes; using Kavita.Common; +using Kavita.Server.Extensions; +using Kavita.Services; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; -namespace API.Controllers; +namespace Kavita.Server.Controllers; [Route("[controller]")] -public class OidcController(ILogger logger, [FromServices] ConfigurationManager? configurationManager = null): ControllerBase +public class OidcController([FromServices] ConfigurationManager? configurationManager = null): ControllerBase { - [SkipDeviceTracking] [AllowAnonymous] + [SkipDeviceTracking] [HttpGet("login")] public IActionResult Login(string returnUrl = "/") { - if (returnUrl == "/") + if (returnUrl == "/" || !Url.IsLocalUrl(returnUrl)) { returnUrl = Configuration.BaseUrl; } diff --git a/API/Controllers/PanelsController.cs b/Kavita.Server/Controllers/PanelsController.cs similarity index 54% rename from API/Controllers/PanelsController.cs rename to Kavita.Server/Controllers/PanelsController.cs index eb039c1bd..4ad7ac2d9 100644 --- a/API/Controllers/PanelsController.cs +++ b/Kavita.Server/Controllers/PanelsController.cs @@ -1,29 +1,17 @@ using System.Threading.Tasks; -using API.Data; -using API.DTOs.Progress; -using API.Services.Reading; +using Kavita.API.Database; +using Kavita.API.Services.Reading; +using Kavita.Models.DTOs.Progress; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; /// /// For the Panels app explicitly /// -[AllowAnonymous] -public class PanelsController : BaseApiController +public class PanelsController(IReaderService readerService, IUnitOfWork unitOfWork) : BaseApiController { - private readonly IReaderService _readerService; - private readonly IUnitOfWork _unitOfWork; - - public PanelsController(IReaderService readerService, IUnitOfWork unitOfWork) - { - _readerService = readerService; - _unitOfWork = unitOfWork; - } - /// /// Saves the progress of a given chapter. /// @@ -33,9 +21,7 @@ public class PanelsController : BaseApiController [HttpPost("save-progress")] public async Task SaveProgress(ProgressDto dto, [FromQuery] string apiKey) { - if (string.IsNullOrEmpty(apiKey)) return Unauthorized("ApiKey is required"); - var userId = await _unitOfWork.UserRepository.GetUserIdByAuthKeyAsync(apiKey); - await _readerService.SaveReadingProgress(dto, userId); + await readerService.SaveReadingProgress(dto, UserId); return Ok(); } @@ -48,10 +34,7 @@ public class PanelsController : BaseApiController [HttpGet("get-progress")] public async Task> GetProgress(int chapterId, [FromQuery] string apiKey) { - if (string.IsNullOrEmpty(apiKey)) return Unauthorized("ApiKey is required"); - var userId = await _unitOfWork.UserRepository.GetUserIdByAuthKeyAsync(apiKey); - - var progress = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(chapterId, userId); + var progress = await unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(chapterId, UserId); if (progress == null) return Ok(new ProgressDto() { PageNum = 0, diff --git a/API/Controllers/PersonController.cs b/Kavita.Server/Controllers/PersonController.cs similarity index 59% rename from API/Controllers/PersonController.cs rename to Kavita.Server/Controllers/PersonController.cs index 9f4638a7c..3281e6f96 100644 --- a/API/Controllers/PersonController.cs +++ b/Kavita.Server/Controllers/PersonController.cs @@ -1,61 +1,46 @@ using System.Collections.Generic; using System.Linq; -using System.Text.RegularExpressions; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Metadata; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Metadata.Browse; -using API.DTOs.Metadata.Browse.Requests; -using API.DTOs.Person; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers; -using API.Services; -using API.Services.Plus; -using API.Services.Tasks.Metadata; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; using AutoMapper; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Metadata; +using Kavita.API.Services.SignalR; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Metadata.Browse; +using Kavita.Models.DTOs.Metadata.Browse.Requests; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities.Enums; +using Kavita.Server.Attributes; +using Kavita.Server.Extensions; +using Kavita.Services.Plus; +using Kavita.Services.Scanner; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Nager.ArticleNumber; -namespace API.Controllers; -#nullable enable - -public class PersonController : BaseApiController +namespace Kavita.Server.Controllers; +public class PersonController( + IUnitOfWork unitOfWork, + ILocalizationService localizationService, + IMapper mapper, + ICoverDbService coverDbService, + IImageService imageService, + IEventHub eventHub, + IPersonService personService) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; - private readonly IMapper _mapper; - private readonly ICoverDbService _coverDbService; - private readonly IImageService _imageService; - private readonly IEventHub _eventHub; - private readonly IPersonService _personService; - - public PersonController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IMapper mapper, - ICoverDbService coverDbService, IImageService imageService, IEventHub eventHub, IPersonService personService) - { - _unitOfWork = unitOfWork; - _localizationService = localizationService; - _mapper = mapper; - _coverDbService = coverDbService; - _imageService = imageService; - _eventHub = eventHub; - _personService = personService; - } - - [HttpGet] public async Task> GetPersonByName(string name) { - var person = await _unitOfWork.PersonRepository.GetPersonDtoByName(name, UserId); + var person = await unitOfWork.PersonRepository.GetPersonDtoByName(name, UserId); if (person == null) return NotFound(); - person.Roles = (await _unitOfWork.PersonRepository.GetRolesForPersonByName(person.Id, UserId)).ToList(); + person.Roles = (await unitOfWork.PersonRepository.GetRolesForPersonByName(person.Id, UserId)).ToList(); EnrichWithWebLinks(person); @@ -101,7 +86,7 @@ public class PersonController : BaseApiController [HttpGet("search")] public async Task>> SearchPeople([FromQuery] string queryString) { - return Ok(await _unitOfWork.PersonRepository.SearchPeople(queryString)); + return Ok(await unitOfWork.PersonRepository.SearchPeople(queryString)); } /// @@ -112,13 +97,14 @@ public class PersonController : BaseApiController [HttpGet("roles")] public async Task>> GetRolesForPersonByName(int personId) { - return Ok(await _unitOfWork.PersonRepository.GetRolesForPersonByName(personId, UserId)); + return Ok(await unitOfWork.PersonRepository.GetRolesForPersonByName(personId, UserId)); } /// /// Returns a list of authors and artists for browsing /// + /// /// /// [HttpPost("all")] @@ -126,7 +112,7 @@ public class PersonController : BaseApiController { userParams ??= UserParams.Default; - var list = await _unitOfWork.PersonRepository.GetBrowsePersonDtos(UserId, filter, userParams); + var list = await unitOfWork.PersonRepository.GetBrowsePersonDtos(UserId, filter, userParams); Response.AddPaginationHeader(list.CurrentPage, list.PageSize, list.TotalCount, list.TotalPages); return Ok(list); @@ -137,29 +123,29 @@ public class PersonController : BaseApiController /// /// /// - [Authorize(PolicyGroups.AdminPolicy)] [HttpPost("update")] + [Authorize(PolicyGroups.AdminPolicy)] public async Task> UpdatePerson(UpdatePersonDto dto) { // This needs to get all people and update them equally - var person = await _unitOfWork.PersonRepository.GetPersonById(dto.Id, PersonIncludes.Aliases); - if (person == null) return BadRequest(_localizationService.Translate(UserId, "person-doesnt-exist")); + var person = await unitOfWork.PersonRepository.GetPersonById(dto.Id, PersonIncludes.Aliases); + if (person == null) return BadRequest(localizationService.Translate(UserId, "person-doesnt-exist")); - if (string.IsNullOrEmpty(dto.Name)) return BadRequest(await _localizationService.Translate(UserId, "person-name-required")); + if (string.IsNullOrEmpty(dto.Name)) return BadRequest(await localizationService.Translate(UserId, "person-name-required")); // Validate the name is unique - if (dto.Name != person.Name && !(await _unitOfWork.PersonRepository.IsNameUnique(dto.Name))) + if (dto.Name != person.Name && !(await unitOfWork.PersonRepository.IsNameUnique(dto.Name))) { - return BadRequest(await _localizationService.Translate(UserId, "person-name-unique")); + return BadRequest(await localizationService.Translate(UserId, "person-name-unique")); } // Update name first, in case it got moved to aliases person.Name = dto.Name.Trim(); person.NormalizedName = person.Name.ToNormalized(); - var success = await _personService.UpdatePersonAliasesAsync(person, dto.Aliases); - if (!success) return BadRequest(await _localizationService.Translate(UserId, "aliases-have-overlap")); + var success = await personService.UpdatePersonAliasesAsync(person, dto.Aliases); + if (!success) return BadRequest(await localizationService.Translate(UserId, "aliases-have-overlap")); person.Description = dto.Description ?? string.Empty; @@ -185,10 +171,10 @@ public class PersonController : BaseApiController person.Asin = asin; } - _unitOfWork.PersonRepository.Update(person); - await _unitOfWork.CommitAsync(); + unitOfWork.PersonRepository.Update(person); + await unitOfWork.CommitAsync(); - return Ok(_mapper.Map(person)); + return Ok(mapper.Map(person)); } /// @@ -196,26 +182,28 @@ public class PersonController : BaseApiController /// /// /// + [PersonAccess] [HttpPost("fetch-cover")] public async Task> DownloadCoverImage([FromQuery] int personId) { - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - var person = await _unitOfWork.PersonRepository.GetPersonById(personId); - if (person == null) return BadRequest(_localizationService.Translate(UserId, "person-doesnt-exist")); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var person = await unitOfWork.PersonRepository.GetPersonById(personId); + if (person == null) return BadRequest(localizationService.Translate(UserId, "person-doesnt-exist")); - var personImage = await _coverDbService.DownloadPersonImageAsync(person, settings.EncodeMediaAs); + var personImage = await coverDbService.DownloadPersonImageAsync(person, settings.EncodeMediaAs); if (string.IsNullOrEmpty(personImage)) { - return BadRequest(await _localizationService.Translate(UserId, "person-image-doesnt-exist")); + return BadRequest(await localizationService.Translate(UserId, "person-image-doesnt-exist")); } person.CoverImage = personImage; - _imageService.UpdateColorScape(person); - _unitOfWork.PersonRepository.Update(person); - await _unitOfWork.CommitAsync(); - await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(person.Id, "person"), false); + imageService.UpdateColorScape(person); + unitOfWork.PersonRepository.Update(person); + + await unitOfWork.CommitAsync(); + await eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(person.Id, "person"), false); return Ok(personImage); } @@ -225,10 +213,11 @@ public class PersonController : BaseApiController /// /// /// + [PersonAccess] [HttpGet("series-known-for")] public async Task>> GetKnownSeries(int personId) { - return Ok(await _unitOfWork.PersonRepository.GetSeriesKnownFor(personId, UserId)); + return Ok(await unitOfWork.PersonRepository.GetSeriesKnownFor(personId, UserId)); } /// @@ -237,10 +226,11 @@ public class PersonController : BaseApiController /// /// /// + [PersonAccess] [HttpGet("chapters-by-role")] public async Task>> GetChaptersByRole(int personId, PersonRole role) { - return Ok(await _unitOfWork.PersonRepository.GetChaptersForPersonByRole(personId, UserId, role)); + return Ok(await unitOfWork.PersonRepository.GetChaptersForPersonByRole(personId, UserId, role)); } /// @@ -252,16 +242,16 @@ public class PersonController : BaseApiController [Authorize(PolicyGroups.AdminPolicy)] public async Task> MergePeople(PersonMergeDto dto) { - var dst = await _unitOfWork.PersonRepository.GetPersonById(dto.DestId, PersonIncludes.All); + var dst = await unitOfWork.PersonRepository.GetPersonById(dto.DestId, PersonIncludes.All); if (dst == null) return BadRequest(); - var src = await _unitOfWork.PersonRepository.GetPersonById(dto.SrcId, PersonIncludes.All); + var src = await unitOfWork.PersonRepository.GetPersonById(dto.SrcId, PersonIncludes.All); if (src == null) return BadRequest(); - await _personService.MergePeopleAsync(src, dst); - await _eventHub.SendMessageAsync(MessageFactory.PersonMerged, MessageFactory.PersonMergedMessage(dst, src)); + await personService.MergePeopleAsync(src, dst); + await eventHub.SendMessageAsync(MessageFactory.PersonMerged, MessageFactory.PersonMergedMessage(dst, src)); - return Ok(_mapper.Map(dst)); + return Ok(mapper.Map(dst)); } /// @@ -272,11 +262,11 @@ public class PersonController : BaseApiController [HttpPost("valid-alias")] public async Task> IsValidAlias(PersonAliasCheckDto dto) { - var person = await _unitOfWork.PersonRepository.GetPersonById(dto.PersonId, PersonIncludes.Aliases); + var person = await unitOfWork.PersonRepository.GetPersonById(dto.PersonId, PersonIncludes.Aliases); if (person == null) return NotFound(); var aliasIsName = dto.Name.ToNormalized() == dto.Alias.ToNormalized(); - var existingAlias = await _unitOfWork.PersonRepository.AnyAliasExist(dto.Alias); + var existingAlias = await unitOfWork.PersonRepository.AnyAliasExist(dto.Alias); return Ok(!existingAlias && !aliasIsName); } diff --git a/API/Controllers/PluginController.cs b/Kavita.Server/Controllers/PluginController.cs similarity index 93% rename from API/Controllers/PluginController.cs rename to Kavita.Server/Controllers/PluginController.cs index 3a06fb06c..e9a7f74f1 100644 --- a/API/Controllers/PluginController.cs +++ b/Kavita.Server/Controllers/PluginController.cs @@ -1,24 +1,22 @@ using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.DTOs; -using API.DTOs.Misc; -using API.Entities.Enums; -using API.Middleware; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Account; +using Kavita.Models.DTOs.Misc; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Scanner; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; [SkipDeviceTracking] public class PluginController(IUnitOfWork unitOfWork, ITokenService tokenService, ILogger logger) @@ -86,7 +84,7 @@ public class PluginController(IUnitOfWork unitOfWork, ITokenService tokenService /// Will always return null if the Auth Key does not belong to this account /// [HttpGet("authkey-expires")] - public async Task> GetAuthKeyExpiration() + public async Task> GetAuthKeyExpiration() { var authKey = AuthKey; if (string.IsNullOrEmpty(authKey)) @@ -94,7 +92,7 @@ public class PluginController(IUnitOfWork unitOfWork, ITokenService tokenService var exp = await unitOfWork.UserRepository.GetAuthKeyExpiration(authKey, UserId); - return Ok(new { ExpiresAt = exp?.ToUniversalTime() }); + return Ok(new AuthKeyExpiresAtDto { ExpiresAt = exp?.ToUniversalTime() }); } diff --git a/API/Controllers/RatingController.cs b/Kavita.Server/Controllers/RatingController.cs similarity index 57% rename from API/Controllers/RatingController.cs rename to Kavita.Server/Controllers/RatingController.cs index d377a33c8..a04493056 100644 --- a/API/Controllers/RatingController.cs +++ b/Kavita.Server/Controllers/RatingController.cs @@ -1,32 +1,24 @@ using System; using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.Services; -using API.Services.Plus; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.Models.DTOs; +using Kavita.Models.Entities.Enums; +using Kavita.Server.Attributes; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; /// /// Responsible for providing external ratings for Series /// -public class RatingController : BaseApiController +public class RatingController( + IUnitOfWork unitOfWork, + IRatingService ratingService, + ILocalizationService localizationService) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly IRatingService _ratingService; - private readonly ILocalizationService _localizationService; - - public RatingController(IUnitOfWork unitOfWork, IRatingService ratingService, ILocalizationService localizationService) - { - _unitOfWork = unitOfWork; - _ratingService = ratingService; - _localizationService = localizationService; - } - /// /// Update the users' rating of the given series /// @@ -36,15 +28,18 @@ public class RatingController : BaseApiController [HttpPost("series")] public async Task UpdateSeriesRating(UpdateRatingDto updateRating) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Ratings | AppUserIncludes.ChapterRatings); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Ratings | AppUserIncludes.ChapterRatings); if (user == null) throw new UnauthorizedAccessException(); - if (await _ratingService.UpdateSeriesRating(user, updateRating)) + if (!await unitOfWork.UserRepository.HasAccessToSeries(UserId, updateRating.SeriesId)) + return NotFound(); + + if (await ratingService.UpdateSeriesRating(user, updateRating)) { return Ok(); } - return BadRequest(await _localizationService.Translate(UserId, "generic-error")); + return BadRequest(await localizationService.Translate(UserId, "generic-error")); } /// @@ -56,15 +51,18 @@ public class RatingController : BaseApiController [HttpPost("chapter")] public async Task UpdateChapterRating(UpdateRatingDto updateRating) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Ratings | AppUserIncludes.ChapterRatings); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Ratings | AppUserIncludes.ChapterRatings); if (user == null) throw new UnauthorizedAccessException(); - if (await _ratingService.UpdateChapterRating(user, updateRating)) + if (!await unitOfWork.UserRepository.HasAccessToSeries(UserId, updateRating.SeriesId)) + return NotFound(); + + if (await ratingService.UpdateChapterRating(user, updateRating)) { return Ok(); } - return BadRequest(await _localizationService.Translate(UserId, "generic-error")); + return BadRequest(await localizationService.Translate(UserId, "generic-error")); } /// @@ -72,13 +70,14 @@ public class RatingController : BaseApiController /// /// /// + [SeriesAccess] [HttpGet("overall-series")] public async Task> GetOverallSeriesRating(int seriesId) { return Ok(new RatingDto() { Provider = ScrobbleProvider.Kavita, - AverageScore = await _unitOfWork.SeriesRepository.GetAverageUserRating(seriesId, UserId), + AverageScore = await unitOfWork.SeriesRepository.GetAverageUserRating(seriesId, UserId), FavoriteCount = 0, }); } @@ -88,13 +87,14 @@ public class RatingController : BaseApiController /// /// /// + [ChapterAccess] [HttpGet("overall-chapter")] public async Task> GetOverallChapterRating(int chapterId) { return Ok(new RatingDto() { Provider = ScrobbleProvider.Kavita, - AverageScore = await _unitOfWork.ChapterRepository.GetAverageUserRating(chapterId, UserId), + AverageScore = await unitOfWork.ChapterRepository.GetAverageUserRating(chapterId, UserId), FavoriteCount = 0, }); } diff --git a/API/Controllers/ReaderController.cs b/Kavita.Server/Controllers/ReaderController.cs similarity index 61% rename from API/Controllers/ReaderController.cs rename to Kavita.Server/Controllers/ReaderController.cs index a7672e58d..765af4767 100644 --- a/API/Controllers/ReaderController.cs +++ b/Kavita.Server/Controllers/ReaderController.cs @@ -3,70 +3,46 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Filtering.v2; -using API.DTOs.Progress; -using API.DTOs.Reader; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Progress; -using API.Middleware; -using API.Services; -using API.Services.Plus; -using API.Services.Reading; -using API.Services.Tasks.Metadata; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; using Hangfire; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; +using Kavita.Server.Attributes; +using Kavita.Services; +using Kavita.Services.Metadata; +using Kavita.Services.Reading; +using Kavita.Services.Scanner; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using MimeTypes; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; /// /// For all things regarding reading, mainly focusing on non-Book related entities /// -public class ReaderController : BaseApiController +/// +public class ReaderController(ICacheService cacheService, + IUnitOfWork unitOfWork, ILogger logger, + IReaderService readerService, IBookmarkService bookmarkService, IEventHub eventHub, + IScrobblingService scrobblingService, + ILocalizationService localizationService, + IBookService bookService) : BaseApiController { - private readonly ICacheService _cacheService; - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - private readonly IReaderService _readerService; - private readonly IBookmarkService _bookmarkService; - private readonly IAccountService _accountService; - private readonly IEventHub _eventHub; - private readonly IScrobblingService _scrobblingService; - private readonly ILocalizationService _localizationService; - private readonly IBookService _bookService; - - /// - public ReaderController(ICacheService cacheService, - IUnitOfWork unitOfWork, ILogger logger, - IReaderService readerService, IBookmarkService bookmarkService, - IAccountService accountService, IEventHub eventHub, - IScrobblingService scrobblingService, - ILocalizationService localizationService, - IBookService bookService) - { - _cacheService = cacheService; - _unitOfWork = unitOfWork; - _logger = logger; - _readerService = readerService; - _bookmarkService = bookmarkService; - _accountService = accountService; - _eventHub = eventHub; - _scrobblingService = scrobblingService; - _localizationService = localizationService; - _bookService = bookService; - } /// /// Returns the PDF for the chapterId. @@ -75,28 +51,24 @@ public class ReaderController : BaseApiController /// Auth Key for authentication /// Converts PDF into images per-page - Used for Mihon mainly /// - [HttpGet("pdf")] + [ChapterAccess] [SkipDeviceTracking] + [HttpGet("pdf")] public async Task GetPdf(int chapterId, string apiKey, bool extractPdf = false) { if (!UserContext.IsAuthenticated) return Unauthorized(); - var chapter = await _cacheService.Ensure(chapterId, extractPdf); + var chapter = await cacheService.Ensure(chapterId, extractPdf); if (chapter == null) return NoContent(); - // Validate the user has access to the PDF - var series = await _unitOfWork.SeriesRepository.GetSeriesForChapter(chapter.Id, - await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(Username!)); - if (series == null) return BadRequest(await _localizationService.Translate(UserId, "invalid-access")); - try { - var path = _cacheService.GetCachedFile(chapter); + var path = cacheService.GetCachedFile(chapter); return CachedFile(path, maxAge: TimeSpan.FromHours(1).Seconds); } catch (Exception) { - _cacheService.CleanupChapters([chapterId]); + cacheService.CleanupChapters([chapterId]); throw; } } @@ -110,24 +82,24 @@ public class ReaderController : BaseApiController /// User's API Key for authentication /// Should Kavita extract pdf into images. Defaults to false. /// - [HttpGet("image")] + [ChapterAccess] [SkipDeviceTracking] - [AllowAnonymous] + [HttpGet("image")] public async Task GetImage(int chapterId, int page, string apiKey, bool extractPdf = false) { if (page < 0) page = 0; try { - var chapter = await _cacheService.Ensure(chapterId, extractPdf); + var chapter = await cacheService.Ensure(chapterId, extractPdf); if (chapter == null) return NoContent(); - var path = _cacheService.GetCachedPagePath(chapter.Id, page); + var path = cacheService.GetCachedPagePath(chapter.Id, page); return CachedFile(path, maxAge: TimeSpan.FromHours(1).Seconds); } catch (Exception) { - _cacheService.CleanupChapters([chapterId]); + cacheService.CleanupChapters([chapterId]); throw; } } @@ -139,16 +111,17 @@ public class ReaderController : BaseApiController /// /// /// - [HttpGet("thumbnail")] + [ChapterAccess] [SkipDeviceTracking] - [AllowAnonymous] + [HttpGet("thumbnail")] public async Task GetThumbnail(int chapterId, int pageNum, string apiKey) { - var chapter = await _cacheService.Ensure(chapterId, true); + var chapter = await cacheService.Ensure(chapterId, true); if (chapter == null) return NoContent(); - var images = _cacheService.GetCachedPages(chapterId); - var path = await _readerService.GetThumbnail(chapter, pageNum, images); + var images = cacheService.GetCachedPages(chapterId); + + var path = await readerService.GetThumbnail(chapter, pageNum, images); return CachedFile(path, maxAge: TimeSpan.FromHours(1).Seconds); } @@ -160,13 +133,13 @@ public class ReaderController : BaseApiController /// /// We must use api key as bookmarks could be leaked to other users via the API /// - [HttpGet("bookmark-image")] + [SeriesAccess] [SkipDeviceTracking] - [AllowAnonymous] + [HttpGet("bookmark-image")] public async Task GetBookmarkImage(int seriesId, string apiKey, int page) { if (page < 0) page = 0; - var totalPages = await _cacheService.CacheBookmarkForSeries(UserId, seriesId); + var totalPages = await cacheService.CacheBookmarkForSeries(UserId, seriesId); if (page > totalPages) { page = totalPages; @@ -174,12 +147,12 @@ public class ReaderController : BaseApiController try { - var path = _cacheService.GetCachedBookmarkPagePath(seriesId, page); + var path = cacheService.GetCachedBookmarkPagePath(seriesId, page); return CachedFile(path, maxAge: TimeSpan.FromHours(1).Seconds); } catch (Exception) { - _cacheService.CleanupBookmarks([seriesId]); + cacheService.CleanupBookmarks([seriesId]); throw; } } @@ -192,16 +165,17 @@ public class ReaderController : BaseApiController /// /// /// - [HttpGet("file-dimensions")] + [ChapterAccess] [SkipDeviceTracking] + [HttpGet("file-dimensions")] [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId", "extractPdf"])] public async Task>> GetFileDimensions(int chapterId, bool extractPdf = false) { if (chapterId <= 0) return ArraySegment.Empty; - var chapter = await _cacheService.Ensure(chapterId, extractPdf); + var chapter = await cacheService.Ensure(chapterId, extractPdf); if (chapter == null) return NoContent(); - return Ok(_cacheService.GetCachedFileDimensions(_cacheService.GetCachePath(chapterId))); + return Ok(cacheService.GetCachedFileDimensions(cacheService.GetCachePath(chapterId))); } /// @@ -212,19 +186,20 @@ public class ReaderController : BaseApiController /// Should Kavita extract pdf into images. Defaults to false. /// Include file dimensions. Only useful for image-based reading /// + [ChapterAccess] [HttpGet("chapter-info")] [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["chapterId", "extractPdf", "includeDimensions"])] public async Task> GetChapterInfo(int chapterId, bool extractPdf = false, bool includeDimensions = false) { if (chapterId <= 0) return Ok(null); // This can happen occasionally from UI, we should just ignore - var chapter = await _cacheService.Ensure(chapterId, extractPdf); + var chapter = await cacheService.Ensure(chapterId, extractPdf); if (chapter == null) return NoContent(); - var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId); - if (dto == null) return BadRequest(await _localizationService.Translate(UserId, "perform-scan")); + var dto = await unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId); + if (dto == null) return BadRequest(await localizationService.Translate(UserId, "perform-scan")); var mangaFile = chapter.Files.First(); - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(dto.SeriesId, UserId); + var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(dto.SeriesId, UserId); if (series == null) return Unauthorized(); var info = new ChapterInfoDto() @@ -248,8 +223,8 @@ public class ReaderController : BaseApiController if (includeDimensions) { - info.PageDimensions = _cacheService.GetCachedFileDimensions(_cacheService.GetCachePath(chapterId)); - info.DoublePairs = _readerService.GetPairs(info.PageDimensions); + info.PageDimensions = cacheService.GetCachedFileDimensions(cacheService.GetCachePath(chapterId)); + info.DoublePairs = readerService.GetPairs(info.PageDimensions); } if (info.ChapterTitle is {Length: > 0}) { @@ -266,7 +241,7 @@ public class ReaderController : BaseApiController } else { - info.Subtitle = await _localizationService.Translate(UserId, "volume-num", info.VolumeNumber); + info.Subtitle = await localizationService.Translate(UserId, "volume-num", info.VolumeNumber); if (!Parser.IsDefaultChapter(info.ChapterNumber)) { info.Subtitle += " " + ReaderService.FormatChapterName(info.LibraryType, true, true) + @@ -284,12 +259,13 @@ public class ReaderController : BaseApiController /// Series Id for all bookmarks /// Include file dimensions (extra I/O). Defaults to true. /// + [SeriesAccess] [HttpGet("bookmark-info")] [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["seriesId", "includeDimensions"])] public async Task> GetBookmarkInfo(int seriesId, bool includeDimensions = true) { - var totalPages = await _cacheService.CacheBookmarkForSeries(UserId, seriesId); - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.None); + var totalPages = await cacheService.CacheBookmarkForSeries(UserId, seriesId); + var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.None); var info = new BookmarkInfoDto() { @@ -302,8 +278,8 @@ public class ReaderController : BaseApiController if (includeDimensions) { - info.PageDimensions = _cacheService.GetCachedFileDimensions(_cacheService.GetBookmarkCachePath(seriesId)); - info.DoublePairs = _readerService.GetPairs(info.PageDimensions); + info.PageDimensions = cacheService.GetCachedFileDimensions(cacheService.GetBookmarkCachePath(seriesId)); + info.DoublePairs = readerService.GetPairs(info.PageDimensions); } return Ok(info); @@ -318,21 +294,21 @@ public class ReaderController : BaseApiController [HttpPost("mark-read")] public async Task MarkRead(MarkReadDto markReadDto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); if (user == null) return Unauthorized(); try { - await _readerService.MarkSeriesAsRead(user, markReadDto.SeriesId); + await readerService.MarkSeriesAsRead(user, markReadDto.SeriesId); } catch (KavitaException ex) { - return BadRequest(await _localizationService.Translate(UserId, ex.Message)); + return BadRequest(await localizationService.Translate(UserId, ex.Message)); } - if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(UserId, "generic-read-progress")); + if (!await unitOfWork.CommitAsync()) return BadRequest(await localizationService.Translate(UserId, "generic-read-progress")); - BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, markReadDto.SeriesId)); - BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(markReadDto.SeriesId, user.Id)); + BackgroundJob.Enqueue(() => scrobblingService.ScrobbleReadingUpdate(user.Id, markReadDto.SeriesId)); + BackgroundJob.Enqueue(() => unitOfWork.SeriesRepository.ClearOnDeckRemoval(markReadDto.SeriesId, user.Id)); return Ok(); } @@ -345,13 +321,13 @@ public class ReaderController : BaseApiController [HttpPost("mark-unread")] public async Task MarkUnread(MarkReadDto markReadDto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); if (user == null) return Unauthorized(); - await _readerService.MarkSeriesAsUnread(user, markReadDto.SeriesId); + await readerService.MarkSeriesAsUnread(user, markReadDto.SeriesId); - if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(UserId, "generic-read-progress")); + if (!await unitOfWork.CommitAsync()) return BadRequest(await localizationService.Translate(UserId, "generic-read-progress")); - BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, markReadDto.SeriesId)); + BackgroundJob.Enqueue(() => scrobblingService.ScrobbleReadingUpdate(user.Id, markReadDto.SeriesId)); return Ok(); } @@ -363,15 +339,15 @@ public class ReaderController : BaseApiController [HttpPost("mark-volume-unread")] public async Task MarkVolumeAsUnread(MarkVolumeReadDto markVolumeReadDto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); if (user == null) return Unauthorized(); - var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId); - await _readerService.MarkChaptersAsUnread(user, markVolumeReadDto.SeriesId, chapters); + var chapters = await unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId); + await readerService.MarkChaptersAsUnread(user, markVolumeReadDto.SeriesId, chapters); - if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(UserId, "generic-read-progress")); + if (!await unitOfWork.CommitAsync()) return BadRequest(await localizationService.Translate(UserId, "generic-read-progress")); - BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, markVolumeReadDto.SeriesId)); + BackgroundJob.Enqueue(() => scrobblingService.ScrobbleReadingUpdate(user.Id, markVolumeReadDto.SeriesId)); return Ok(); } @@ -383,29 +359,29 @@ public class ReaderController : BaseApiController [HttpPost("mark-volume-read")] public async Task MarkVolumeAsRead(MarkVolumeReadDto markVolumeReadDto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); - var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId); + var chapters = await unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId); if (user == null) return Unauthorized(); try { - await _readerService.MarkChaptersAsRead(user, markVolumeReadDto.SeriesId, chapters); + await readerService.MarkChaptersAsRead(user, markVolumeReadDto.SeriesId, chapters); } catch (KavitaException ex) { - return BadRequest(await _localizationService.Translate(UserId, ex.Message)); + return BadRequest(await localizationService.Translate(UserId, ex.Message)); } - if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(UserId, "generic-read-progress")); + if (!await unitOfWork.CommitAsync()) return BadRequest(await localizationService.Translate(UserId, "generic-read-progress")); - await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate, + await eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate, MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName!, markVolumeReadDto.SeriesId, markVolumeReadDto.VolumeId, 0, chapters.Sum(c => c.Pages))); - BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, markVolumeReadDto.SeriesId)); - BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(markVolumeReadDto.SeriesId, user.Id)); + BackgroundJob.Enqueue(() => scrobblingService.ScrobbleReadingUpdate(user.Id, markVolumeReadDto.SeriesId)); + BackgroundJob.Enqueue(() => unitOfWork.SeriesRepository.ClearOnDeckRemoval(markVolumeReadDto.SeriesId, user.Id)); return Ok(); } @@ -418,23 +394,23 @@ public class ReaderController : BaseApiController [HttpPost("mark-multiple-read")] public async Task MarkMultipleAsRead(MarkVolumesReadDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); if (user == null) return Unauthorized(); user.Progresses ??= []; - var chapterIds = await _unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds); + var chapterIds = await unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds); foreach (var chapterId in dto.ChapterIds) { chapterIds.Add(chapterId); } - var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds); - await _readerService.MarkChaptersAsRead(user, dto.SeriesId, chapters.ToList()); + var chapters = await unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds); + await readerService.MarkChaptersAsRead(user, dto.SeriesId, chapters.ToList()); - if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(UserId, "generic-read-progress")); + if (!await unitOfWork.CommitAsync()) return BadRequest(await localizationService.Translate(UserId, "generic-read-progress")); - BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, dto.SeriesId)); - BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(dto.SeriesId, user.Id)); + BackgroundJob.Enqueue(() => scrobblingService.ScrobbleReadingUpdate(user.Id, dto.SeriesId)); + BackgroundJob.Enqueue(() => unitOfWork.SeriesRepository.ClearOnDeckRemoval(dto.SeriesId, user.Id)); return Ok(); } @@ -447,25 +423,25 @@ public class ReaderController : BaseApiController [HttpPost("mark-multiple-unread")] public async Task MarkMultipleAsUnread(MarkVolumesReadDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); if (user == null) return Unauthorized(); user.Progresses ??= new List(); - var chapterIds = await _unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds); + var chapterIds = await unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds); foreach (var chapterId in dto.ChapterIds) { chapterIds.Add(chapterId); } - var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds); - await _readerService.MarkChaptersAsUnread(user, dto.SeriesId, chapters.ToList()); + var chapters = await unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds); + await readerService.MarkChaptersAsUnread(user, dto.SeriesId, chapters.ToList()); - if (await _unitOfWork.CommitAsync()) + if (await unitOfWork.CommitAsync()) { - BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, dto.SeriesId)); + BackgroundJob.Enqueue(() => scrobblingService.ScrobbleReadingUpdate(user.Id, dto.SeriesId)); return Ok(); } - return BadRequest(await _localizationService.Translate(UserId, "generic-read-progress")); + return BadRequest(await localizationService.Translate(UserId, "generic-read-progress")); } /// @@ -476,22 +452,22 @@ public class ReaderController : BaseApiController [HttpPost("mark-multiple-series-read")] public async Task MarkMultipleSeriesAsRead(MarkMultipleSeriesAsReadDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); if (user == null) return Unauthorized(); user.Progresses ??= new List(); - var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(dto.SeriesIds.ToArray(), true); + var volumes = await unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(dto.SeriesIds.ToArray(), true); foreach (var volume in volumes) { - await _readerService.MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters); + await readerService.MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters); } - if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(UserId, "generic-read-progress")); + if (!await unitOfWork.CommitAsync()) return BadRequest(await localizationService.Translate(UserId, "generic-read-progress")); foreach (var sId in dto.SeriesIds) { - BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, sId)); - BackgroundJob.Enqueue(() => _unitOfWork.SeriesRepository.ClearOnDeckRemoval(sId, user.Id)); + BackgroundJob.Enqueue(() => scrobblingService.ScrobbleReadingUpdate(user.Id, sId)); + BackgroundJob.Enqueue(() => unitOfWork.SeriesRepository.ClearOnDeckRemoval(sId, user.Id)); } return Ok(); } @@ -504,26 +480,26 @@ public class ReaderController : BaseApiController [HttpPost("mark-multiple-series-unread")] public async Task MarkMultipleSeriesAsUnread(MarkMultipleSeriesAsReadDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress); if (user == null) return Unauthorized(); user.Progresses ??= []; - var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(dto.SeriesIds.ToArray(), true); + var volumes = await unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(dto.SeriesIds.ToArray(), true); foreach (var volume in volumes) { - await _readerService.MarkChaptersAsUnread(user, volume.SeriesId, volume.Chapters); + await readerService.MarkChaptersAsUnread(user, volume.SeriesId, volume.Chapters); } - if (await _unitOfWork.CommitAsync()) + if (await unitOfWork.CommitAsync()) { foreach (var sId in dto.SeriesIds) { - BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleReadingUpdate(user.Id, sId)); + BackgroundJob.Enqueue(() => scrobblingService.ScrobbleReadingUpdate(user.Id, sId)); } return Ok(); } - return BadRequest(await _localizationService.Translate(UserId, "generic-read-progress")); + return BadRequest(await localizationService.Translate(UserId, "generic-read-progress")); } /// @@ -531,11 +507,12 @@ public class ReaderController : BaseApiController /// /// /// + [ChapterAccess] [HttpGet("get-progress")] public async Task> GetProgress(int chapterId) { - var progress = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(chapterId, UserId); - _logger.LogDebug("Get Progress for {ChapterId} is {Pages}", chapterId, progress?.PageNum ?? 0); + var progress = await unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(chapterId, UserId); + logger.LogDebug("Get Progress for {ChapterId} is {Pages}", chapterId, progress?.PageNum ?? 0); if (progress == null) return Ok(new ProgressDto() { @@ -558,9 +535,9 @@ public class ReaderController : BaseApiController { var userId = UserId; - if (!await _readerService.SaveReadingProgress(progressDto, userId)) + if (!await readerService.SaveReadingProgress(progressDto, userId)) { - return BadRequest(await _localizationService.Translate(userId, "generic-read-progress")); + return BadRequest(await localizationService.Translate(userId, "generic-read-progress")); } return Ok(); @@ -571,10 +548,11 @@ public class ReaderController : BaseApiController /// Otherwise, loop through the chapters and volumes in order to find the next chapter which has progress. /// /// + [SeriesAccess] [HttpGet("continue-point")] public async Task> GetContinuePoint(int seriesId) { - return Ok(await _readerService.GetContinuePoint(seriesId, UserId)); + return Ok(await readerService.GetContinuePoint(seriesId, UserId)); } /// @@ -582,10 +560,11 @@ public class ReaderController : BaseApiController /// /// /// + [SeriesAccess] [HttpGet("has-progress")] public async Task> HasProgress(int seriesId) { - return Ok(await _unitOfWork.AppUserProgressRepository.HasAnyProgressOnSeriesAsync(seriesId, UserId)); + return Ok(await unitOfWork.AppUserProgressRepository.HasAnyProgressOnSeriesAsync(seriesId, UserId)); } /// @@ -593,10 +572,11 @@ public class ReaderController : BaseApiController /// /// /// + [ChapterAccess] [HttpGet("chapter-bookmarks")] public async Task>> GetBookmarks(int chapterId) { - return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForChapter(UserId, chapterId)); + return Ok(await unitOfWork.UserRepository.GetBookmarkDtosForChapter(UserId, chapterId)); } /// @@ -607,7 +587,7 @@ public class ReaderController : BaseApiController [HttpPost("all-bookmarks")] public async Task>> GetAllBookmarks(FilterV2Dto filterDto) { - return Ok(await _unitOfWork.UserRepository.GetAllBookmarkDtos(UserId, filterDto)); + return Ok(await unitOfWork.UserRepository.GetAllBookmarkDtos(UserId, filterDto)); } /// @@ -618,36 +598,36 @@ public class ReaderController : BaseApiController [HttpPost("remove-bookmarks")] public async Task RemoveBookmarks(RemoveBookmarkForSeriesDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Bookmarks); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Bookmarks); if (user == null) return Unauthorized(); - if (user.Bookmarks == null || user.Bookmarks.Count == 0) return Ok(await _localizationService.Translate(UserId, "nothing-to-do")); + if (user.Bookmarks == null || user.Bookmarks.Count == 0) return Ok(await localizationService.Translate(UserId, "nothing-to-do")); try { var bookmarksToRemove = user.Bookmarks.Where(bmk => bmk.SeriesId == dto.SeriesId).ToList(); user.Bookmarks = user.Bookmarks.Where(bmk => bmk.SeriesId != dto.SeriesId).ToList(); - _unitOfWork.UserRepository.Update(user); + unitOfWork.UserRepository.Update(user); - if (!_unitOfWork.HasChanges() || await _unitOfWork.CommitAsync()) + if (!unitOfWork.HasChanges() || await unitOfWork.CommitAsync()) { try { - await _bookmarkService.DeleteBookmarkFiles(bookmarksToRemove); + await bookmarkService.DeleteBookmarkFiles(bookmarksToRemove); } catch (Exception ex) { - _logger.LogError(ex, "There was an issue cleaning up old bookmarks"); + logger.LogError(ex, "There was an issue cleaning up old bookmarks"); } return Ok(); } } catch (Exception ex) { - _logger.LogError(ex, "There was an exception when trying to clear bookmarks"); - await _unitOfWork.RollbackAsync(); + logger.LogError(ex, "There was an exception when trying to clear bookmarks"); + await unitOfWork.RollbackAsync(); } - return BadRequest(await _localizationService.Translate(UserId, "generic-clear-bookmarks")); + return BadRequest(await localizationService.Translate(UserId, "generic-clear-bookmarks")); } /// @@ -658,9 +638,9 @@ public class ReaderController : BaseApiController [HttpPost("bulk-remove-bookmarks")] public async Task BulkRemoveBookmarks(BulkRemoveBookmarkForSeriesDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Bookmarks); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Bookmarks); if (user == null) return Unauthorized(); - if (user.Bookmarks == null || user.Bookmarks.Count == 0) return Ok(await _localizationService.Translate(UserId, "nothing-to-do")); + if (user.Bookmarks == null || user.Bookmarks.Count == 0) return Ok(await localizationService.Translate(UserId, "nothing-to-do")); try { @@ -668,23 +648,23 @@ public class ReaderController : BaseApiController { var bookmarksToRemove = user.Bookmarks.Where(bmk => bmk.SeriesId == seriesId).ToList(); user.Bookmarks = user.Bookmarks.Where(bmk => bmk.SeriesId != seriesId).ToList(); - _unitOfWork.UserRepository.Update(user); - await _bookmarkService.DeleteBookmarkFiles(bookmarksToRemove); + unitOfWork.UserRepository.Update(user); + await bookmarkService.DeleteBookmarkFiles(bookmarksToRemove); } - if (!_unitOfWork.HasChanges() || await _unitOfWork.CommitAsync()) + if (!unitOfWork.HasChanges() || await unitOfWork.CommitAsync()) { return Ok(); } } catch (Exception ex) { - _logger.LogError(ex, "There was an exception when trying to clear bookmarks"); - await _unitOfWork.RollbackAsync(); + logger.LogError(ex, "There was an exception when trying to clear bookmarks"); + await unitOfWork.RollbackAsync(); } - return BadRequest(await _localizationService.Translate(UserId, "generic-clear-bookmarks")); + return BadRequest(await localizationService.Translate(UserId, "generic-clear-bookmarks")); } /// @@ -692,10 +672,11 @@ public class ReaderController : BaseApiController /// /// /// + [VolumeAccess] [HttpGet("volume-bookmarks")] public async Task>> GetBookmarksForVolume(int volumeId) { - return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForVolume(UserId, volumeId)); + return Ok(await unitOfWork.UserRepository.GetBookmarkDtosForVolume(UserId, volumeId)); } /// @@ -703,10 +684,11 @@ public class ReaderController : BaseApiController /// /// /// + [SeriesAccess] [HttpGet("series-bookmarks")] public async Task>> GetBookmarksForSeries(int seriesId) { - return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForSeries(UserId, seriesId)); + return Ok(await unitOfWork.UserRepository.GetBookmarkDtosForSeries(UserId, seriesId)); } /// @@ -716,41 +698,38 @@ public class ReaderController : BaseApiController /// /// [HttpPost("bookmark")] + [Authorize(PolicyGroups.BookmarkPolicy)] public async Task BookmarkPage(BookmarkDto bookmarkDto) { try { // Don't let user save past total pages. - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, - AppUserIncludes.Bookmarks); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Bookmarks); if (user == null) return new UnauthorizedResult(); - if (!await _accountService.HasBookmarkPermission(user)) - return BadRequest(await _localizationService.Translate(UserId, "bookmark-permission")); - - var chapter = await _cacheService.Ensure(bookmarkDto.ChapterId); + var chapter = await cacheService.Ensure(bookmarkDto.ChapterId); if (chapter == null || chapter.Files.Count == 0) - return BadRequest(await _localizationService.Translate(UserId, "cache-file-find")); + return BadRequest(await localizationService.Translate(UserId, "cache-file-find")); - bookmarkDto.Page = _readerService.CapPageToChapter(chapter, bookmarkDto.Page); + bookmarkDto.Page = readerService.CapPageToChapter(chapter, bookmarkDto.Page); string path; string? chapterTitle; if (Parser.IsEpub(chapter.Files.First().Extension!)) { - var cachedFilePath = _cacheService.GetCachedFile(chapter); - path = await _bookService.CopyImageToTempFromBook(chapter.Id, bookmarkDto, cachedFilePath); + var cachedFilePath = cacheService.GetCachedFile(chapter); + path = await bookService.CopyImageToTempFromBook(chapter.Id, bookmarkDto, cachedFilePath); - var chapterEntity = await _unitOfWork.ChapterRepository.GetChapterAsync(bookmarkDto.ChapterId); - if (chapterEntity == null) return BadRequest(await _localizationService.Translate(UserId, "chapter-doesnt-exist")); - var toc = await _bookService.GenerateTableOfContents(chapterEntity); + var chapterEntity = await unitOfWork.ChapterRepository.GetChapterAsync(bookmarkDto.ChapterId); + if (chapterEntity == null) return BadRequest(await localizationService.Translate(UserId, "chapter-doesnt-exist")); + var toc = await bookService.GenerateTableOfContents(chapterEntity); chapterTitle = BookService.GetChapterTitleFromToC(toc, bookmarkDto.Page); } else { - path = _cacheService.GetCachedPagePath(chapter.Id, bookmarkDto.Page); + path = cacheService.GetCachedPagePath(chapter.Id, bookmarkDto.Page); chapterTitle = chapter.TitleName; } @@ -758,20 +737,20 @@ public class ReaderController : BaseApiController - if (string.IsNullOrEmpty(path) || !await _bookmarkService.BookmarkPage(user, bookmarkDto, path)) + if (string.IsNullOrEmpty(path) || !await bookmarkService.BookmarkPage(user, bookmarkDto, path)) { - return BadRequest(await _localizationService.Translate(UserId, "bookmark-save")); + return BadRequest(await localizationService.Translate(UserId, "bookmark-save")); } - BackgroundJob.Enqueue(() => _cacheService.CleanupBookmarkCache(bookmarkDto.SeriesId)); + BackgroundJob.Enqueue(() => cacheService.CleanupBookmarkCache(bookmarkDto.SeriesId)); return Ok(); } catch (KavitaException ex) { - _logger.LogError(ex, "There was an exception when trying to create a bookmark"); - return BadRequest(await _localizationService.Translate(UserId, "bookmark-save")); + logger.LogError(ex, "There was an exception when trying to create a bookmark"); + return BadRequest(await localizationService.Translate(UserId, "bookmark-save")); } } @@ -782,24 +761,21 @@ public class ReaderController : BaseApiController /// /// [HttpPost("unbookmark")] + [Authorize(PolicyGroups.BookmarkPolicy)] public async Task UnBookmarkPage(BookmarkDto bookmarkDto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Bookmarks); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Bookmarks); if (user == null) return new UnauthorizedResult(); + if (user.Bookmarks == null || user.Bookmarks.Count == 0) return Ok(); - if (!await _accountService.HasBookmarkPermission(user)) + if (!await bookmarkService.RemoveBookmarkPage(user, bookmarkDto)) { - return BadRequest(await _localizationService.Translate(UserId, "bookmark-permission")); - } - - if (!await _bookmarkService.RemoveBookmarkPage(user, bookmarkDto)) - { - return BadRequest(await _localizationService.Translate(UserId, "bookmark-save")); + return BadRequest(await localizationService.Translate(UserId, "bookmark-save")); } - BackgroundJob.Enqueue(() => _cacheService.CleanupBookmarkCache(bookmarkDto.SeriesId)); + BackgroundJob.Enqueue(() => cacheService.CleanupBookmarkCache(bookmarkDto.SeriesId)); return Ok(); } @@ -814,11 +790,12 @@ public class ReaderController : BaseApiController /// /// /// chapter id for next manga - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["seriesId", "volumeId", "currentChapterId"])] + [SeriesAccess] [HttpGet("next-chapter")] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["seriesId", "volumeId", "currentChapterId"])] public async Task> GetNextChapter(int seriesId, int volumeId, int currentChapterId) { - return Ok(await _readerService.GetNextChapterIdAsync(seriesId, volumeId, currentChapterId, UserId)); + return Ok(await readerService.GetNextChapterIdAsync(seriesId, volumeId, currentChapterId, UserId)); } @@ -832,11 +809,12 @@ public class ReaderController : BaseApiController /// /// /// chapter id for next manga - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["seriesId", "volumeId", "currentChapterId"])] + [SeriesAccess] [HttpGet("prev-chapter")] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["seriesId", "volumeId", "currentChapterId"])] public async Task> GetPreviousChapter(int seriesId, int volumeId, int currentChapterId) { - return Ok(await _readerService.GetPrevChapterIdAsync(seriesId, volumeId, currentChapterId, UserId)); + return Ok(await readerService.GetPrevChapterIdAsync(seriesId, volumeId, currentChapterId, UserId)); } /// @@ -845,20 +823,21 @@ public class ReaderController : BaseApiController /// For Epubs, this does not check words inside a chapter due to overhead so may not work in all cases. /// /// + [SeriesAccess] [HttpGet("time-left")] [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["seriesId"])] public async Task> GetEstimateToCompletion(int seriesId) { var userId = UserId; - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); - if (series == null) return BadRequest(await _localizationService.Translate(UserId, "series-doesnt-exist")); + var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); + if (series == null) return BadRequest(await localizationService.Translate(UserId, "series-doesnt-exist")); // Get all sum of all chapters with progress that is complete then subtract from series. Multiply by modifiers - var progress = await _unitOfWork.AppUserProgressRepository.GetUserProgressForSeriesAsync(seriesId, userId); + var progress = await unitOfWork.AppUserProgressRepository.GetUserProgressForSeriesAsync(seriesId, userId); if (series.Format == MangaFormat.Epub) { var chapters = - await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(progress.Select(p => p.ChapterId).ToList()); + await unitOfWork.ChapterRepository.GetChaptersByIdsAsync(progress.Select(p => p.ChapterId).ToList()); // Word count var progressCount = chapters.Sum(c => c.WordCount); var wordsLeft = series.WordCount - progressCount; @@ -879,19 +858,20 @@ public class ReaderController : BaseApiController /// /// /// + [SeriesAccess] [HttpGet("time-left-for-chapter")] [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["seriesId", "chapterId"])] public async Task> GetEstimateToCompletionForChapter(int seriesId, int chapterId) { var userId = UserId; - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); - var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId, userId); - if (series == null || chapter == null) return BadRequest(await _localizationService.Translate(UserId, "generic-error")); + var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); + var chapter = await unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId, userId); + if (series == null || chapter == null) return BadRequest(await localizationService.Translate(UserId, "generic-error")); if (series.Format == MangaFormat.Epub) { // Get the word counts for all the pages - var pageCounts = await _bookService.GetWordCountsPerPage(chapter.Files.First().FilePath); // TODO: Cache + var pageCounts = await bookService.GetWordCountsPerPage(chapter.Files.First().FilePath); // TODO: Cache if (pageCounts == null) return ReaderService.GetTimeEstimate(series.WordCount, 0, true); // Sum character counts only for pages that have been read @@ -916,10 +896,11 @@ public class ReaderController : BaseApiController /// /// /// + [ChapterAccess] [HttpGet("ptoc")] public ActionResult> GetPersonalToC(int chapterId) { - return Ok(_unitOfWork.UserTableOfContentRepository.GetPersonalToC(UserId, chapterId)); + return Ok(unitOfWork.UserTableOfContentRepository.GetPersonalToC(UserId, chapterId)); } /// @@ -929,18 +910,19 @@ public class ReaderController : BaseApiController /// /// /// + [ChapterAccess] [HttpDelete("ptoc")] public async Task DeletePersonalToc([FromQuery] int chapterId, [FromQuery] int pageNum, [FromQuery] string title) { var userId = UserId; - if (string.IsNullOrWhiteSpace(title)) return BadRequest(await _localizationService.Translate(userId, "name-required")); - if (pageNum < 0) return BadRequest(await _localizationService.Translate(userId, "valid-number")); + if (string.IsNullOrWhiteSpace(title)) return BadRequest(await localizationService.Translate(userId, "name-required")); + if (pageNum < 0) return BadRequest(await localizationService.Translate(userId, "valid-number")); - var toc = await _unitOfWork.UserTableOfContentRepository.Get(userId, chapterId, pageNum, title); + var toc = await unitOfWork.UserTableOfContentRepository.Get(userId, chapterId, pageNum, title); if (toc == null) return Ok(); - _unitOfWork.UserTableOfContentRepository.Remove(toc); - await _unitOfWork.CommitAsync(); + unitOfWork.UserTableOfContentRepository.Remove(toc); + await unitOfWork.CommitAsync(); return Ok(); } @@ -956,21 +938,24 @@ public class ReaderController : BaseApiController { // Validate there isn't already an existing page title combo? var userId = UserId; - if (string.IsNullOrWhiteSpace(dto.Title)) return BadRequest(await _localizationService.Translate(userId, "name-required")); - if (dto.PageNumber < 0) return BadRequest(await _localizationService.Translate(userId, "valid-number")); - if (await _unitOfWork.UserTableOfContentRepository.IsUnique(userId, dto.ChapterId, dto.PageNumber, + if (string.IsNullOrWhiteSpace(dto.Title)) return BadRequest(await localizationService.Translate(userId, "name-required")); + + if (!await unitOfWork.UserRepository.HasAccessToChapter(UserId, dto.ChapterId)) return NotFound(); + + if (dto.PageNumber < 0) return BadRequest(await localizationService.Translate(userId, "valid-number")); + if (await unitOfWork.UserTableOfContentRepository.IsUnique(userId, dto.ChapterId, dto.PageNumber, dto.Title.Trim())) { - return BadRequest(await _localizationService.Translate(userId, "duplicate-bookmark")); + return BadRequest(await localizationService.Translate(userId, "duplicate-bookmark")); } // Look up the chapter this PTOC is associated with to get the chapter title (if there is one) - var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(dto.ChapterId); - if (chapter == null) return BadRequest(await _localizationService.Translate(userId, "chapter-doesnt-exist")); - var toc = await _bookService.GenerateTableOfContents(chapter); + var chapter = await unitOfWork.ChapterRepository.GetChapterAsync(dto.ChapterId); + if (chapter == null) return BadRequest(await localizationService.Translate(userId, "chapter-doesnt-exist")); + var toc = await bookService.GenerateTableOfContents(chapter); var chapterTitle = BookService.GetChapterTitleFromToC(toc, dto.PageNumber); - _unitOfWork.UserTableOfContentRepository.Attach(new AppUserTableOfContent() + unitOfWork.UserTableOfContentRepository.Attach(new AppUserTableOfContent() { Title = dto.Title.Trim(), ChapterId = dto.ChapterId, @@ -982,7 +967,7 @@ public class ReaderController : BaseApiController ChapterTitle = chapterTitle, AppUserId = userId }); - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); return Ok(); } @@ -991,11 +976,13 @@ public class ReaderController : BaseApiController /// Check if we should prompt the user for rereads for the given series /// /// + /// /// + [SeriesAccess] [HttpGet("prompt-reread/series")] public async Task> ShouldPromptForSeriesReRead(int seriesId, int libraryId) { - return Ok(await _readerService.CheckSeriesForReRead(UserId, seriesId, libraryId)); + return Ok(await readerService.CheckSeriesForReRead(UserId, seriesId, libraryId)); } /// @@ -1005,10 +992,11 @@ public class ReaderController : BaseApiController /// /// /// + [SeriesAccess] [HttpGet("prompt-reread/volume")] public async Task> ShouldPromptForVolumeReRead(int libraryId, int seriesId, int volumeId) { - return Ok(await _readerService.CheckVolumeForReRead(UserId, volumeId, seriesId, libraryId)); + return Ok(await readerService.CheckVolumeForReRead(UserId, volumeId, seriesId, libraryId)); } /// @@ -1018,16 +1006,17 @@ public class ReaderController : BaseApiController /// /// /// + [SeriesAccess] [HttpGet("prompt-reread/chapter")] public async Task> ShouldPromptForChapterReRead(int libraryId, int seriesId, int chapterId) { - return Ok(await _readerService.CheckChapterForReRead(UserId, chapterId, seriesId, libraryId)); + return Ok(await readerService.CheckChapterForReRead(UserId, chapterId, seriesId, libraryId)); } [HttpGet("first-progress-date")] public async Task> GetFirstReadingDate(int userId) { - return Ok(await _unitOfWork.AppUserProgressRepository.GetFirstProgressForUser(userId)); + return Ok(await unitOfWork.AppUserProgressRepository.GetFirstProgressForUser(userId)); } } diff --git a/API/Controllers/ReadingListController.cs b/Kavita.Server/Controllers/ReadingListController.cs similarity index 55% rename from API/Controllers/ReadingListController.cs rename to Kavita.Server/Controllers/ReadingListController.cs index a9d98f03e..e3b2134eb 100644 --- a/API/Controllers/ReadingListController.cs +++ b/Kavita.Server/Controllers/ReadingListController.cs @@ -1,42 +1,32 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Person; -using API.DTOs.ReadingLists; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers; -using API.Middleware; -using API.Services; -using API.Services.Reading; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Reading; using Kavita.Common; +using Kavita.Common.Helpers; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.Entities.Enums; +using Kavita.Server.Attributes; +using Kavita.Server.Extensions; +using Kavita.Services.Reading; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; [Authorize] -public class ReadingListController : BaseApiController +public class ReadingListController( + IUnitOfWork unitOfWork, + IReadingListService readingListService, + ILocalizationService localizationService) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly IReadingListService _readingListService; - private readonly ILocalizationService _localizationService; - private readonly IReaderService _readerService; - - public ReadingListController(IUnitOfWork unitOfWork, IReadingListService readingListService, - ILocalizationService localizationService, IReaderService readerService) - { - _unitOfWork = unitOfWork; - _readingListService = readingListService; - _localizationService = localizationService; - _readerService = readerService; - } - /// /// Fetches a single Reading List /// @@ -45,10 +35,10 @@ public class ReadingListController : BaseApiController [HttpGet] public async Task> GetList(int readingListId) { - var readingList = await _unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, UserId); + var readingList = await unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, UserId); if (readingList == null) { - return BadRequest(await _localizationService.Translate(UserId, "reading-list-restricted")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-restricted")); } return Ok(readingList); @@ -65,7 +55,7 @@ public class ReadingListController : BaseApiController public async Task>> GetListsForUser([FromQuery] UserParams userParams, bool includePromoted = true, bool sortByLastModified = false) { - var items = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(UserId, includePromoted, + var items = await unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(UserId, includePromoted, userParams, sortByLastModified); Response.AddPaginationHeader(items.CurrentPage, items.PageSize, items.TotalCount, items.TotalPages); @@ -80,7 +70,7 @@ public class ReadingListController : BaseApiController [HttpGet("lists-for-series")] public async Task>> GetListsForSeries(int seriesId) { - return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtosForSeriesAndUserAsync(UserId, + return Ok(await unitOfWork.ReadingListRepository.GetReadingListDtosForSeriesAndUserAsync(UserId, seriesId, true)); } @@ -92,7 +82,7 @@ public class ReadingListController : BaseApiController [HttpGet("lists-for-chapter")] public async Task>> GetListsForChapter(int chapterId) { - return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtosForChapterAndUserAsync(UserId, + return Ok(await unitOfWork.ReadingListRepository.GetReadingListDtosForChapterAndUserAsync(UserId, chapterId, true)); } @@ -105,7 +95,7 @@ public class ReadingListController : BaseApiController [HttpGet("items")] public async Task>> GetListForUser(int readingListId) { - return Ok(await _readingListService.GetReadingListItems(readingListId, UserId)); + return Ok(await readingListService.GetReadingListItems(readingListId, UserId)); } @@ -119,16 +109,16 @@ public class ReadingListController : BaseApiController public async Task UpdateListItemPosition(UpdateReadingListPosition dto) { // Make sure UI buffers events - var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); + var user = await readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); if (user == null) { - return BadRequest(await _localizationService.Translate(UserId, "reading-list-permission")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-permission")); } - if (await _readingListService.UpdateReadingListItemPosition(dto)) return Ok(await _localizationService.Translate(UserId, "reading-list-updated")); + if (await readingListService.UpdateReadingListItemPosition(dto)) return Ok(await localizationService.Translate(UserId, "reading-list-updated")); - return BadRequest(await _localizationService.Translate(UserId, "reading-list-position")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-position")); } /// @@ -140,18 +130,18 @@ public class ReadingListController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task DeleteListItem(UpdateReadingListPosition dto) { - var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); + var user = await readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); if (user == null) { - return BadRequest(await _localizationService.Translate(UserId, "reading-list-permission")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-permission")); } - if (await _readingListService.DeleteReadingListItem(dto)) + if (await readingListService.DeleteReadingListItem(dto)) { - return Ok(await _localizationService.Translate(UserId, "reading-list-updated")); + return Ok(await localizationService.Translate(UserId, "reading-list-updated")); } - return BadRequest(await _localizationService.Translate(UserId, "reading-list-item-delete")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-item-delete")); } /// @@ -163,18 +153,18 @@ public class ReadingListController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task DeleteReadFromList([FromQuery] int readingListId) { - var user = await _readingListService.UserHasReadingListAccess(readingListId, Username!); + var user = await readingListService.UserHasReadingListAccess(readingListId, Username!); if (user == null) { - return BadRequest(await _localizationService.Translate(UserId, "reading-list-permission")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-permission")); } - if (await _readingListService.RemoveFullyReadItems(readingListId, user)) + if (await readingListService.RemoveFullyReadItems(readingListId, user)) { - return Ok(await _localizationService.Translate(UserId, "reading-list-updated")); + return Ok(await localizationService.Translate(UserId, "reading-list-updated")); } - return BadRequest(await _localizationService.Translate(UserId, "reading-list-item-delete")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-item-delete")); } /// @@ -186,16 +176,16 @@ public class ReadingListController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task DeleteList([FromQuery] int readingListId) { - var user = await _readingListService.UserHasReadingListAccess(readingListId, Username!); + var user = await readingListService.UserHasReadingListAccess(readingListId, Username!); if (user == null) { - return BadRequest(await _localizationService.Translate(UserId, "reading-list-permission")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-permission")); } - if (await _readingListService.DeleteReadingList(readingListId, user)) - return Ok(await _localizationService.Translate(UserId, "reading-list-deleted")); + if (await readingListService.DeleteReadingList(readingListId, user)) + return Ok(await localizationService.Translate(UserId, "reading-list-deleted")); - return BadRequest(await _localizationService.Translate(UserId, "generic-reading-list-delete")); + return BadRequest(await localizationService.Translate(UserId, "generic-reading-list-delete")); } /// @@ -207,19 +197,19 @@ public class ReadingListController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> CreateList(CreateReadingListDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.ReadingLists); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.ReadingLists); if (user == null) return Unauthorized(); try { - await _readingListService.CreateReadingListForUser(user, dto.Title); + await readingListService.CreateReadingListForUser(user, dto.Title); } catch (KavitaException ex) { - return BadRequest(await _localizationService.Translate(UserId, ex.Message)); + return BadRequest(await localizationService.Translate(UserId, ex.Message)); } - return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByTitleAsync(user.Id, dto.Title)); + return Ok(await unitOfWork.ReadingListRepository.GetReadingListDtoByTitleAsync(user.Id, dto.Title)); } /// @@ -231,25 +221,25 @@ public class ReadingListController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task UpdateList(UpdateReadingListDto dto) { - var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId); - if (readingList == null) return BadRequest(await _localizationService.Translate(UserId, "reading-list-doesnt-exist")); + var readingList = await unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId); + if (readingList == null) return BadRequest(await localizationService.Translate(UserId, "reading-list-doesnt-exist")); - var user = await _readingListService.UserHasReadingListAccess(readingList.Id, Username!); + var user = await readingListService.UserHasReadingListAccess(readingList.Id, Username!); if (user == null) { - return BadRequest(await _localizationService.Translate(UserId, "reading-list-permission")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-permission")); } try { - await _readingListService.UpdateReadingList(readingList, dto); + await readingListService.UpdateReadingList(readingList, dto); } catch (KavitaException ex) { - return BadRequest(await _localizationService.Translate(UserId, ex.Message)); + return BadRequest(await localizationService.Translate(UserId, ex.Message)); } - return Ok(await _localizationService.Translate(UserId, "reading-list-updated")); + return Ok(await localizationService.Translate(UserId, "reading-list-updated")); } /// @@ -261,37 +251,37 @@ public class ReadingListController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task UpdateListBySeries(UpdateReadingListBySeriesDto dto) { - var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); + var user = await readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); if (user == null) { - return BadRequest(await _localizationService.Translate(UserId, "reading-list-permission")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-permission")); } var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId); - if (readingList == null) return BadRequest(await _localizationService.Translate(UserId, "reading-list-doesnt-exist")); + if (readingList == null) return BadRequest(await localizationService.Translate(UserId, "reading-list-doesnt-exist")); var chapterIdsForSeries = - await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync([dto.SeriesId]); + await unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync([dto.SeriesId]); // If there are adds, tell tracking this has been modified - if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, chapterIdsForSeries, readingList)) + if (await readingListService.AddChaptersToReadingList(dto.SeriesId, chapterIdsForSeries, readingList)) { - _unitOfWork.ReadingListRepository.Update(readingList); + unitOfWork.ReadingListRepository.Update(readingList); } try { - if (_unitOfWork.HasChanges()) + if (unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); - return Ok(await _localizationService.Translate(UserId, "reading-list-updated")); + await unitOfWork.CommitAsync(); + return Ok(await localizationService.Translate(UserId, "reading-list-updated")); } } catch { - await _unitOfWork.RollbackAsync(); + await unitOfWork.RollbackAsync(); } - return Ok(await _localizationService.Translate(UserId, "nothing-to-do")); + return Ok(await localizationService.Translate(UserId, "nothing-to-do")); } @@ -304,40 +294,40 @@ public class ReadingListController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task UpdateListByMultiple(UpdateReadingListByMultipleDto dto) { - var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); + var user = await readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); if (user == null) { - return BadRequest(await _localizationService.Translate(UserId, "reading-list-permission")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-permission")); } var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId); - if (readingList == null) return BadRequest(await _localizationService.Translate(UserId, "reading-list-doesnt-exist")); + if (readingList == null) return BadRequest(await localizationService.Translate(UserId, "reading-list-doesnt-exist")); - var chapterIds = await _unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds); + var chapterIds = await unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds); foreach (var chapterId in dto.ChapterIds) { chapterIds.Add(chapterId); } // If there are adds, tell tracking this has been modified - if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, chapterIds, readingList)) + if (await readingListService.AddChaptersToReadingList(dto.SeriesId, chapterIds, readingList)) { - _unitOfWork.ReadingListRepository.Update(readingList); + unitOfWork.ReadingListRepository.Update(readingList); } try { - if (_unitOfWork.HasChanges()) + if (unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); - return Ok(await _localizationService.Translate(UserId, "reading-list-updated")); + await unitOfWork.CommitAsync(); + return Ok(await localizationService.Translate(UserId, "reading-list-updated")); } } catch { - await _unitOfWork.RollbackAsync(); + await unitOfWork.RollbackAsync(); } - return Ok(await _localizationService.Translate(UserId, "nothing-to-do")); + return Ok(await localizationService.Translate(UserId, "nothing-to-do")); } /// @@ -349,110 +339,110 @@ public class ReadingListController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task UpdateListByMultipleSeries(UpdateReadingListByMultipleSeriesDto dto) { - var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); + var user = await readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); if (user == null) { - return BadRequest(await _localizationService.Translate(UserId, "reading-list-permission")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-permission")); } var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId); - if (readingList == null) return BadRequest(await _localizationService.Translate(UserId, "reading-list-doesnt-exist")); + if (readingList == null) return BadRequest(await localizationService.Translate(UserId, "reading-list-doesnt-exist")); - var ids = await _unitOfWork.SeriesRepository.GetChapterIdWithSeriesIdForSeriesAsync(dto.SeriesIds.ToArray()); + var ids = await unitOfWork.SeriesRepository.GetChapterIdWithSeriesIdForSeriesAsync(dto.SeriesIds.ToArray()); foreach (var seriesId in ids.Keys) { // If there are adds, tell tracking this has been modified - if (await _readingListService.AddChaptersToReadingList(seriesId, ids[seriesId], readingList)) + if (await readingListService.AddChaptersToReadingList(seriesId, ids[seriesId], readingList)) { - _unitOfWork.ReadingListRepository.Update(readingList); + unitOfWork.ReadingListRepository.Update(readingList); } } try { - if (_unitOfWork.HasChanges()) + if (unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); - return Ok(await _localizationService.Translate(UserId, "reading-list-updated")); + await unitOfWork.CommitAsync(); + return Ok(await localizationService.Translate(UserId, "reading-list-updated")); } } catch { - await _unitOfWork.RollbackAsync(); + await unitOfWork.RollbackAsync(); } - return Ok(await _localizationService.Translate(UserId, "nothing-to-do")); + return Ok(await localizationService.Translate(UserId, "nothing-to-do")); } [HttpPost("update-by-volume")] [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task UpdateListByVolume(UpdateReadingListByVolumeDto dto) { - var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); + var user = await readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); if (user == null) { - return BadRequest(await _localizationService.Translate(UserId, "reading-list-permission")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-permission")); } var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId); - if (readingList == null) return BadRequest(await _localizationService.Translate(UserId, "reading-list-doesnt-exist")); + if (readingList == null) return BadRequest(await localizationService.Translate(UserId, "reading-list-doesnt-exist")); var chapterIdsForVolume = - (await _unitOfWork.ChapterRepository.GetChaptersAsync(dto.VolumeId)).Select(c => c.Id).ToList(); + (await unitOfWork.ChapterRepository.GetChaptersAsync(dto.VolumeId)).Select(c => c.Id).ToList(); // If there are adds, tell tracking this has been modified - if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, chapterIdsForVolume, readingList)) + if (await readingListService.AddChaptersToReadingList(dto.SeriesId, chapterIdsForVolume, readingList)) { - _unitOfWork.ReadingListRepository.Update(readingList); + unitOfWork.ReadingListRepository.Update(readingList); } try { - if (_unitOfWork.HasChanges()) + if (unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); - return Ok(await _localizationService.Translate(UserId, "reading-list-updated")); + await unitOfWork.CommitAsync(); + return Ok(await localizationService.Translate(UserId, "reading-list-updated")); } } catch { - await _unitOfWork.RollbackAsync(); + await unitOfWork.RollbackAsync(); } - return Ok(await _localizationService.Translate(UserId, "nothing-to-do")); + return Ok(await localizationService.Translate(UserId, "nothing-to-do")); } [HttpPost("update-by-chapter")] [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task UpdateListByChapter(UpdateReadingListByChapterDto dto) { - var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); + var user = await readingListService.UserHasReadingListAccess(dto.ReadingListId, Username!); if (user == null) { - return BadRequest(await _localizationService.Translate(UserId, "reading-list-permission")); + return BadRequest(await localizationService.Translate(UserId, "reading-list-permission")); } var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId); - if (readingList == null) return BadRequest(await _localizationService.Translate(UserId, "reading-list-doesnt-exist")); + if (readingList == null) return BadRequest(await localizationService.Translate(UserId, "reading-list-doesnt-exist")); // If there are adds, tell tracking this has been modified - if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, new List() { dto.ChapterId }, readingList)) + if (await readingListService.AddChaptersToReadingList(dto.SeriesId, new List() { dto.ChapterId }, readingList)) { - _unitOfWork.ReadingListRepository.Update(readingList); + unitOfWork.ReadingListRepository.Update(readingList); } try { - if (_unitOfWork.HasChanges()) + if (unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); - return Ok(await _localizationService.Translate(UserId, "reading-list-updated")); + await unitOfWork.CommitAsync(); + return Ok(await localizationService.Translate(UserId, "reading-list-updated")); } } catch { - await _unitOfWork.RollbackAsync(); + await unitOfWork.RollbackAsync(); } - return Ok(await _localizationService.Translate(UserId, "nothing-to-do")); + return Ok(await localizationService.Translate(UserId, "nothing-to-do")); } @@ -462,11 +452,12 @@ public class ReadingListController : BaseApiController /// /// PersonRole /// + [ReadingListAccess] [HttpGet("people")] [ResponseCache(CacheProfileName = ResponseCacheProfiles.TenMinute, VaryByQueryKeys = ["readingListId", "role"])] public ActionResult> GetPeopleByRoleForList(int readingListId, PersonRole role) { - return Ok(_unitOfWork.ReadingListRepository.GetReadingListPeopleAsync(readingListId, role)); + return Ok(unitOfWork.ReadingListRepository.GetReadingListPeopleAsync(readingListId, role)); } /// @@ -474,11 +465,12 @@ public class ReadingListController : BaseApiController /// /// /// + [ReadingListAccess] [HttpGet("all-people")] [ResponseCache(CacheProfileName = ResponseCacheProfiles.TenMinute, VaryByQueryKeys = ["readingListId"])] public async Task>> GetAllPeopleForList(int readingListId) { - return Ok(await _unitOfWork.ReadingListRepository.GetReadingListAllPeopleAsync(readingListId)); + return Ok(await unitOfWork.ReadingListRepository.GetReadingListAllPeopleAsync(readingListId)); } /// @@ -487,12 +479,15 @@ public class ReadingListController : BaseApiController /// /// /// Chapter ID for next item, -1 if nothing exists + [ReadingListAccess] [HttpGet("next-chapter")] public async Task> GetNextChapter(int currentChapterId, int readingListId) { - var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).ToList(); + var items = (await unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).ToList(); + var readingListItem = items.SingleOrDefault(rl => rl.ChapterId == currentChapterId); - if (readingListItem == null) return BadRequest(await _localizationService.Translate(UserId, "chapter-doesnt-exist")); + if (readingListItem == null) return BadRequest(await localizationService.Translate(UserId, "chapter-doesnt-exist")); + var index = items.IndexOf(readingListItem) + 1; if (items.Count > index) { @@ -508,12 +503,15 @@ public class ReadingListController : BaseApiController /// /// /// ChapterId for next item, -1 if nothing exists + [ReadingListAccess] [HttpGet("prev-chapter")] public async Task> GetPrevChapter(int currentChapterId, int readingListId) { - var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).ToList(); + var items = (await unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).ToList(); + var readingListItem = items.SingleOrDefault(rl => rl.ChapterId == currentChapterId); - if (readingListItem == null) return BadRequest(await _localizationService.Translate(UserId, "chapter-doesnt-exist")); + if (readingListItem == null) return BadRequest(await localizationService.Translate(UserId, "chapter-doesnt-exist")); + var index = items.IndexOf(readingListItem) - 1; if (0 <= index) { @@ -528,12 +526,12 @@ public class ReadingListController : BaseApiController /// /// If empty or null, will return true as that is invalid /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpGet("name-exists")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> DoesNameExists(string name) { if (string.IsNullOrEmpty(name)) return true; - return Ok(await _unitOfWork.ReadingListRepository.ReadingListExists(name)); + return Ok(await unitOfWork.ReadingListRepository.ReadingListExists(name)); } @@ -551,20 +549,20 @@ public class ReadingListController : BaseApiController var userId = UserId; if (!User.IsInRole(PolicyConstants.PromoteRole) && !User.IsInRole(PolicyConstants.AdminRole)) { - return BadRequest(await _localizationService.Translate(userId, "permission-denied")); + return BadRequest(await localizationService.Translate(userId, "permission-denied")); } - var readingLists = await _unitOfWork.ReadingListRepository.GetReadingListsByIds(dto.ReadingListIds); + var readingLists = await unitOfWork.ReadingListRepository.GetReadingListsByIds(dto.ReadingListIds); foreach (var readingList in readingLists) { if (readingList.AppUserId != userId) continue; readingList.Promoted = dto.Promoted; - _unitOfWork.ReadingListRepository.Update(readingList); + unitOfWork.ReadingListRepository.Update(readingList); } - if (!_unitOfWork.HasChanges()) return Ok(); - await _unitOfWork.CommitAsync(); + if (!unitOfWork.HasChanges()) return Ok(); + await unitOfWork.CommitAsync(); return Ok(); } @@ -579,15 +577,15 @@ public class ReadingListController : BaseApiController public async Task DeleteMultipleReadingLists(DeleteReadingListsDto dto) { // This needs to take into account owner as I can select other users cards - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.ReadingLists); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.ReadingLists); if (user == null) return Unauthorized(); user.ReadingLists = user.ReadingLists.Where(uc => !dto.ReadingListIds.Contains(uc.Id)).ToList(); - _unitOfWork.UserRepository.Update(user); + unitOfWork.UserRepository.Update(user); - if (!_unitOfWork.HasChanges()) return Ok(); - await _unitOfWork.CommitAsync(); + if (!unitOfWork.HasChanges()) return Ok(); + await unitOfWork.CommitAsync(); return Ok(); } @@ -601,7 +599,7 @@ public class ReadingListController : BaseApiController [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["readingListId"])] public async Task> GetReadingListInfo(int readingListId) { - var result = await _unitOfWork.ReadingListRepository.GetReadingListInfoAsync(readingListId); + var result = await unitOfWork.ReadingListRepository.GetReadingListInfoAsync(readingListId); if (result == null) return Ok(null); diff --git a/API/Controllers/ReadingProfileController.cs b/Kavita.Server/Controllers/ReadingProfileController.cs similarity index 88% rename from API/Controllers/ReadingProfileController.cs rename to Kavita.Server/Controllers/ReadingProfileController.cs index f2eef16f9..e7a792312 100644 --- a/API/Controllers/ReadingProfileController.cs +++ b/Kavita.Server/Controllers/ReadingProfileController.cs @@ -1,16 +1,17 @@ -#nullable enable using System; using System.Collections.Generic; using System.Threading.Tasks; -using API.Data; -using API.DTOs; -using API.Services; -using API.Services.Reading; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Reading; using Kavita.Common; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Server.Attributes; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace API.Controllers; +namespace Kavita.Server.Controllers; public record BulkSetSeriesProfiles(List ProfileIds, List SeriesIds); @@ -38,6 +39,7 @@ public class ReadingProfileController(ILogger logger, /// /// Defaults to currently active device /// + [SeriesAccess] [HttpGet("{libraryId:int}/{seriesId:int}")] public async Task> GetProfileForSeries(int libraryId, int seriesId, [FromQuery] bool skipImplicit, [FromQuery] int? deviceId = null) { @@ -51,6 +53,7 @@ public class ReadingProfileController(ILogger logger, /// /// /// + [SeriesAccess] [HttpGet("series")] public async Task>> GetProfilesForSeries(int seriesId) { @@ -62,6 +65,7 @@ public class ReadingProfileController(ILogger logger, /// /// /// + [LibraryAccess] [HttpGet("library")] public async Task>> GetProfilesForLibrary(int libraryId) { @@ -74,6 +78,7 @@ public class ReadingProfileController(ILogger logger, /// /// [HttpPost("create")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> CreateReadingProfile([FromBody] UserReadingProfileDto dto) { return Ok(await readingProfileService.CreateReadingProfile(UserId, dto)); @@ -83,9 +88,10 @@ public class ReadingProfileController(ILogger logger, /// Promotes the implicit profile to a user profile. Removes the series from other profiles /// /// - /// Defaults to currently active device + /// Defaults to the currently active device /// [HttpPost("promote")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> PromoteImplicitReadingProfile([FromQuery] int profileId, [FromQuery] int? deviceId = null) { deviceId ??= clientInfoAccessor.CurrentDeviceId; @@ -100,9 +106,11 @@ public class ReadingProfileController(ILogger logger, /// /// /// - /// Defaults to currently active device + /// Defaults to the currently active device /// + [SeriesAccess] [HttpPost("series")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> UpdateReadingProfileForSeries( [FromBody] UserReadingProfileDto dto, [FromQuery] int libraryId, [FromQuery] int seriesId, [FromQuery] int? deviceId = null) { @@ -121,6 +129,7 @@ public class ReadingProfileController(ILogger logger, /// Defaults to currently active device /// [HttpPost("update-parent")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> UpdateParentProfileForSeries( [FromBody] UserReadingProfileDto dto, [FromQuery] int libraryId, [FromQuery] int seriesId, [FromQuery] int? deviceId = null) { @@ -139,6 +148,7 @@ public class ReadingProfileController(ILogger logger, /// This does not update connected series and libraries. /// [HttpPost] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> UpdateReadingProfile(UserReadingProfileDto dto) { return Ok(await readingProfileService.UpdateReadingProfile(UserId, dto)); @@ -152,6 +162,7 @@ public class ReadingProfileController(ILogger logger, /// /// [HttpDelete] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task DeleteReadingProfile([FromQuery] int profileId) { await readingProfileService.DeleteReadingProfile(UserId, profileId); @@ -164,7 +175,9 @@ public class ReadingProfileController(ILogger logger, /// /// /// + [SeriesAccess] [HttpPost("series/{seriesId:int}")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task SetSeriesProfiles(int seriesId, List profileIds) { await readingProfileService.SetSeriesProfiles(UserId, profileIds, seriesId); @@ -176,7 +189,9 @@ public class ReadingProfileController(ILogger logger, /// /// /// + [SeriesAccess] [HttpDelete("series/{seriesId:int}")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task ClearSeriesProfile(int seriesId) { await readingProfileService.ClearSeriesProfile(UserId, seriesId); @@ -189,7 +204,9 @@ public class ReadingProfileController(ILogger logger, /// /// /// + [LibraryAccess] [HttpPost("library/{libraryId:int}")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task SetLibraryProfiles(int libraryId, List profileIds) { await readingProfileService.SetLibraryProfiles(UserId, profileIds, libraryId); @@ -201,7 +218,9 @@ public class ReadingProfileController(ILogger logger, /// /// /// + [LibraryAccess] [HttpDelete("library/{libraryId:int}")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task ClearLibraryProfile(int libraryId) { await readingProfileService.ClearLibraryProfile(UserId, libraryId); @@ -214,6 +233,7 @@ public class ReadingProfileController(ILogger logger, /// /// [HttpPost("bulk")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task BulkAddReadingProfile(BulkSetSeriesProfiles body) { await readingProfileService.BulkSetSeriesProfiles(UserId, body.ProfileIds, body.SeriesIds); @@ -227,6 +247,7 @@ public class ReadingProfileController(ILogger logger, /// /// [HttpPost("set-devices")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task SetProfileDevices([FromQuery] int profileId, [FromBody] List deviceIds) { await readingProfileService.SetProfileDevices(UserId, profileId, deviceIds); diff --git a/API/Controllers/ReviewController.cs b/Kavita.Server/Controllers/ReviewController.cs similarity index 56% rename from API/Controllers/ReviewController.cs rename to Kavita.Server/Controllers/ReviewController.cs index 276005486..3773423a4 100644 --- a/API/Controllers/ReviewController.cs +++ b/Kavita.Server/Controllers/ReviewController.cs @@ -1,46 +1,41 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.SeriesDetail; -using API.Helpers.Builders; -using API.Services.Plus; using AutoMapper; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services.Plus; +using Kavita.Models.Builders; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Server.Attributes; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; +namespace Kavita.Server.Controllers; -#nullable enable - -public class ReviewController : BaseApiController +public class ReviewController( + IUnitOfWork unitOfWork, + IMapper mapper, + IScrobblingService scrobblingService) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly IMapper _mapper; - private readonly IScrobblingService _scrobblingService; - - public ReviewController(IUnitOfWork unitOfWork, - IMapper mapper, IScrobblingService scrobblingService) - { - _unitOfWork = unitOfWork; - _mapper = mapper; - _scrobblingService = scrobblingService; - } - - /// /// Updates the user's review for a given series /// /// /// [HttpPost("series")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> UpdateSeriesReview(UpdateUserReviewDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Ratings); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Ratings); if (user == null) return Unauthorized(); - var ratingBuilder = new RatingBuilder(await _unitOfWork.UserRepository.GetUserRatingAsync(dto.SeriesId, user.Id)); + if (!await unitOfWork.UserRepository.HasAccessToSeries(UserId, dto.SeriesId)) + return NotFound(); + + var ratingBuilder = new RatingBuilder(await unitOfWork.UserRepository.GetUserRatingAsync(dto.SeriesId, user.Id)); var rating = ratingBuilder .WithBody(dto.Body) @@ -52,13 +47,13 @@ public class ReviewController : BaseApiController user.Ratings.Add(rating); } - _unitOfWork.UserRepository.Update(user); + unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); BackgroundJob.Enqueue(() => - _scrobblingService.ScrobbleReviewUpdate(user.Id, dto.SeriesId, string.Empty, dto.Body)); - return Ok(_mapper.Map(rating)); + scrobblingService.ScrobbleReviewUpdate(user.Id, dto.SeriesId, string.Empty, dto.Body)); + return Ok(mapper.Map(rating)); } /// @@ -67,16 +62,20 @@ public class ReviewController : BaseApiController /// chapterId must be set /// [HttpPost("chapter")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> UpdateChapterReview(UpdateUserReviewDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.ChapterRatings); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.ChapterRatings); if (user == null) return Unauthorized(); if (dto.ChapterId == null) return BadRequest(); - int chapterId = dto.ChapterId.Value; + if (!await unitOfWork.UserRepository.HasAccessToSeries(UserId, dto.SeriesId)) + return NotFound(); - var ratingBuilder = new ChapterRatingBuilder(await _unitOfWork.UserRepository.GetUserChapterRatingAsync(user.Id, chapterId)); + var chapterId = dto.ChapterId.Value; + + var ratingBuilder = new ChapterRatingBuilder(await unitOfWork.UserRepository.GetUserChapterRatingAsync(user.Id, chapterId)); var rating = ratingBuilder .WithBody(dto.Body) @@ -89,11 +88,11 @@ public class ReviewController : BaseApiController user.ChapterRatings.Add(rating); } - _unitOfWork.UserRepository.Update(user); + unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); - return Ok(_mapper.Map(rating)); + return Ok(mapper.Map(rating)); } @@ -102,16 +101,17 @@ public class ReviewController : BaseApiController /// /// [HttpDelete("series")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task DeleteSeriesReview([FromQuery] int seriesId) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Ratings); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Ratings); if (user == null) return Unauthorized(); user.Ratings = user.Ratings.Where(r => r.SeriesId != seriesId).ToList(); - _unitOfWork.UserRepository.Update(user); + unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); return Ok(); } @@ -121,16 +121,17 @@ public class ReviewController : BaseApiController /// /// [HttpDelete("chapter")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task DeleteChapterReview([FromQuery] int chapterId) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.ChapterRatings); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.ChapterRatings); if (user == null) return Unauthorized(); user.ChapterRatings = user.ChapterRatings.Where(r => r.ChapterId != chapterId).ToList(); - _unitOfWork.UserRepository.Update(user); + unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); return Ok(); } @@ -145,6 +146,6 @@ public class ReviewController : BaseApiController [HttpGet("all")] public async Task>> GetAllReviewsForUser(int userId, float? rating = null, string? filterQuery = null) { - return Ok(await _unitOfWork.UserRepository.GetAllReviewsForUser(userId, UserId, filterQuery, rating)); + return Ok(await unitOfWork.UserRepository.GetAllReviewsForUser(userId, UserId, filterQuery, rating)); } } diff --git a/API/Controllers/ScrobblingController.cs b/Kavita.Server/Controllers/ScrobblingController.cs similarity index 67% rename from API/Controllers/ScrobblingController.cs rename to Kavita.Server/Controllers/ScrobblingController.cs index fc5512ed3..78a759816 100644 --- a/API/Controllers/ScrobblingController.cs +++ b/Kavita.Server/Controllers/ScrobblingController.cs @@ -2,44 +2,35 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs.KavitaPlus.Account; -using API.DTOs.Scrobbling; -using API.Entities.Scrobble; -using API.Extensions; -using API.Helpers; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.Common.Helpers; +using Kavita.Models.Builders; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.KavitaPlus.Account; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Scrobble; +using Kavita.Server.Attributes; +using Kavita.Server.Extensions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Controllers; +namespace Kavita.Server.Controllers; -#nullable enable - -public class ScrobblingController : BaseApiController +public class ScrobblingController( + IUnitOfWork unitOfWork, + IScrobblingService scrobblingService, + ILogger logger, + ILocalizationService localizationService) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly IScrobblingService _scrobblingService; - private readonly ILogger _logger; - private readonly ILocalizationService _localizationService; - - public ScrobblingController(IUnitOfWork unitOfWork, IScrobblingService scrobblingService, - ILogger logger, ILocalizationService localizationService) - { - _unitOfWork = unitOfWork; - _scrobblingService = scrobblingService; - _logger = logger; - _localizationService = localizationService; - } - /// /// Get the current user's AniList token /// @@ -47,7 +38,7 @@ public class ScrobblingController : BaseApiController [HttpGet("anilist-token")] public async Task> GetAniListToken() { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); if (user == null) return Unauthorized(); return Ok(user.AniListAccessToken); @@ -60,7 +51,7 @@ public class ScrobblingController : BaseApiController [HttpGet("mal-token")] public async Task> GetMalToken() { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); if (user == null) return Unauthorized(); return Ok(new MalUserInfoDto() @@ -76,15 +67,16 @@ public class ScrobblingController : BaseApiController /// /// True if the token was new or not [HttpPost("update-anilist-token")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> UpdateAniListToken(AniListUpdateDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); if (user == null) return Unauthorized(); var isNewToken = string.IsNullOrEmpty(user.AniListAccessToken); user.AniListAccessToken = dto.Token; - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(); return Ok(isNewToken); } @@ -95,17 +87,18 @@ public class ScrobblingController : BaseApiController /// /// True if the token was new or not [HttpPost("update-mal-token")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> UpdateMalToken(MalUserInfoDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!); if (user == null) return Unauthorized(); var isNewToken = string.IsNullOrEmpty(user.MalAccessToken); user.MalAccessToken = dto.AccessToken; user.MalUserName = dto.Username; - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(); return Ok(isNewToken); } @@ -115,9 +108,10 @@ public class ScrobblingController : BaseApiController /// /// [HttpPost("generate-scrobble-events")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public ActionResult GenerateScrobbleEvents() { - BackgroundJob.Enqueue(() => _scrobblingService.CreateEventsFromExistingHistory(UserId)); + BackgroundJob.Enqueue(() => scrobblingService.CreateEventsFromExistingHistory(UserId)); return Ok(); } @@ -130,7 +124,7 @@ public class ScrobblingController : BaseApiController [HttpGet("token-expired")] public async Task> HasTokenExpired(ScrobbleProvider provider) { - return Ok(await _scrobblingService.HasTokenExpired(UserId, provider)); + return Ok(await scrobblingService.HasTokenExpired(UserId, provider)); } /// @@ -138,22 +132,22 @@ public class ScrobblingController : BaseApiController /// /// Requires admin /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpGet("scrobble-errors")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task>> GetScrobbleErrors() { - return Ok(await _unitOfWork.ScrobbleRepository.GetScrobbleErrors()); + return Ok(await unitOfWork.ScrobbleRepository.GetScrobbleErrors()); } /// /// Clears the scrobbling errors table /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("clear-errors")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task ClearScrobbleErrors() { - await _unitOfWork.ScrobbleRepository.ClearScrobbleErrors(); + await unitOfWork.ScrobbleRepository.ClearScrobbleErrors(); return Ok(); } @@ -166,7 +160,7 @@ public class ScrobblingController : BaseApiController public async Task>> GetScrobblingEvents([FromQuery] UserParams pagination, [FromBody] ScrobbleEventFilter filter) { pagination ??= UserParams.Default; - var events = await _unitOfWork.ScrobbleRepository.GetUserEvents(UserId, filter, pagination); + var events = await unitOfWork.ScrobbleRepository.GetUserEvents(UserId, filter, pagination); Response.AddPaginationHeader(events.CurrentPage, events.PageSize, events.TotalCount, events.TotalPages); return Ok(events); @@ -179,7 +173,7 @@ public class ScrobblingController : BaseApiController [HttpGet("holds")] public async Task>> GetScrobbleHolds() { - return Ok(await _unitOfWork.UserRepository.GetHolds(UserId)); + return Ok(await unitOfWork.UserRepository.GetHolds(UserId)); } /// @@ -190,7 +184,7 @@ public class ScrobblingController : BaseApiController [HttpGet("has-hold")] public async Task> HasHold(int seriesId) { - return Ok(await _unitOfWork.UserRepository.HasHoldOnSeries(UserId, seriesId)); + return Ok(await unitOfWork.UserRepository.HasHoldOnSeries(UserId, seriesId)); } /// @@ -201,7 +195,7 @@ public class ScrobblingController : BaseApiController [HttpGet("library-allows-scrobbling")] public async Task> LibraryAllowsScrobbling(int seriesId) { - return Ok(await _unitOfWork.LibraryRepository.GetAllowsScrobblingBySeriesId(seriesId)); + return Ok(await unitOfWork.LibraryRepository.GetAllowsScrobblingBySeriesId(seriesId)); } /// @@ -210,25 +204,26 @@ public class ScrobblingController : BaseApiController /// /// [HttpPost("add-hold")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task AddHold(int seriesId) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.ScrobbleHolds); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.ScrobbleHolds); if (user == null) return Unauthorized(); if (user.ScrobbleHolds.Any(s => s.SeriesId == seriesId)) - return Ok(await _localizationService.Translate(user.Id, "nothing-to-do")); + return Ok(await localizationService.Translate(user.Id, "nothing-to-do")); var seriesHold = new ScrobbleHoldBuilder() .WithSeriesId(seriesId) .Build(); user.ScrobbleHolds.Add(seriesHold); - _unitOfWork.UserRepository.Update(user); + unitOfWork.UserRepository.Update(user); try { - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + 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); + await scrobblingService.ClearEventsForSeries(user.Id, seriesId); return Ok(); } catch (DbUpdateConcurrencyException ex) @@ -240,16 +235,16 @@ public class ScrobblingController : BaseApiController } // Retry the update - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(); return Ok(); } catch (Exception ex) { // Handle other exceptions or log the error - _logger.LogError(ex, "An error occurred while adding the hold"); + logger.LogError(ex, "An error occurred while adding the hold"); return StatusCode(StatusCodes.Status500InternalServerError, - await _localizationService.Translate(UserId, "nothing-to-do")); + await localizationService.Translate(UserId, "nothing-to-do")); } } @@ -259,15 +254,16 @@ public class ScrobblingController : BaseApiController /// /// [HttpDelete("remove-hold")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task RemoveHold(int seriesId) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.ScrobbleHolds); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.ScrobbleHolds); if (user == null) return Unauthorized(); user.ScrobbleHolds = user.ScrobbleHolds.Where(h => h.SeriesId != seriesId).ToList(); - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(); return Ok(); } @@ -278,7 +274,7 @@ public class ScrobblingController : BaseApiController [HttpGet("has-ran-scrobble-gen")] public async Task> HasRanScrobbleGen() { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId); return Ok(user is {HasRunScrobbleEventGeneration: true}); } @@ -288,11 +284,12 @@ public class ScrobblingController : BaseApiController /// /// [HttpPost("bulk-remove-events")] + [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task BulkRemoveScrobbleEvents(IList eventIds) { - var events = await _unitOfWork.ScrobbleRepository.GetUserEvents(UserId, eventIds); - _unitOfWork.ScrobbleRepository.Remove(events); - await _unitOfWork.CommitAsync(); + var events = await unitOfWork.ScrobbleRepository.GetUserEvents(UserId, eventIds); + unitOfWork.ScrobbleRepository.Remove(events); + await unitOfWork.CommitAsync(); return Ok(); } } diff --git a/API/Controllers/SearchController.cs b/Kavita.Server/Controllers/SearchController.cs similarity index 56% rename from API/Controllers/SearchController.cs rename to Kavita.Server/Controllers/SearchController.cs index ef1555909..670f1a756 100644 --- a/API/Controllers/SearchController.cs +++ b/Kavita.Server/Controllers/SearchController.cs @@ -1,31 +1,22 @@ -using System.Linq; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Search; -using API.Services; +using System.Threading.Tasks; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Search; +using Kavita.Server.Attributes; +using Kavita.Services.Scanner; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; /// /// Responsible for the Search interface from the UI /// -public class SearchController : BaseApiController +public class SearchController(IUnitOfWork unitOfWork, ILocalizationService localizationService) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; - - public SearchController(IUnitOfWork unitOfWork, ILocalizationService localizationService) - { - _unitOfWork = unitOfWork; - _localizationService = localizationService; - } - /// /// Returns the series for the MangaFile id. If the user does not have access (shouldn't happen by the UI), /// then null is returned @@ -35,7 +26,13 @@ public class SearchController : BaseApiController [HttpGet("series-for-mangafile")] public async Task> GetSeriesForMangaFile(int mangaFileId) { - return Ok(await _unitOfWork.SeriesRepository.GetSeriesForMangaFile(mangaFileId, UserId)); + var series = await unitOfWork.SeriesRepository.GetSeriesForMangaFile(mangaFileId, UserId); + if (series == null) return NotFound(); + + if (!await unitOfWork.UserRepository.HasAccessToSeries(UserId, series.Id)) + return NotFound(); + + return Ok(series); } /// @@ -44,10 +41,11 @@ public class SearchController : BaseApiController /// /// /// + [ChapterAccess] [HttpGet("series-for-chapter")] public async Task> GetSeriesForChapter(int chapterId) { - return Ok(await _unitOfWork.SeriesRepository.GetSeriesForChapter(chapterId, UserId)); + return Ok(await unitOfWork.SeriesRepository.GetSeriesForChapter(chapterId, UserId)); } /// @@ -59,14 +57,14 @@ public class SearchController : BaseApiController [HttpGet("search")] public async Task> Search(string queryString, [FromQuery] bool includeChapterAndFiles = true) { - queryString = Services.Tasks.Scanner.Parser.Parser.CleanQuery(queryString); + queryString = Parser.CleanQuery(queryString); - var libraries = await _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(UserId, QueryContext.Search); - if (libraries.Count == 0) return BadRequest(await _localizationService.Translate(UserId, "libraries-restricted")); + var libraries = await unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(UserId, QueryContext.Search); + if (libraries.Count == 0) return BadRequest(await localizationService.Translate(UserId, "libraries-restricted")); var isAdmin = UserContext.HasRole(PolicyConstants.AdminRole); - var series = await _unitOfWork.SeriesRepository.SearchSeries(UserId, isAdmin, + var series = await unitOfWork.SeriesRepository.SearchSeries(UserId, isAdmin, libraries, queryString, includeChapterAndFiles); return Ok(series); diff --git a/API/Controllers/SeriesController.cs b/Kavita.Server/Controllers/SeriesController.cs similarity index 69% rename from API/Controllers/SeriesController.cs rename to Kavita.Server/Controllers/SeriesController.cs index e9e1e5fcd..69e2747e2 100644 --- a/API/Controllers/SeriesController.cs +++ b/Kavita.Server/Controllers/SeriesController.cs @@ -1,73 +1,52 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Dashboard; -using API.DTOs.Filtering; -using API.DTOs.Filtering.v2; -using API.DTOs.Metadata; -using API.DTOs.Metadata.Matching; -using API.DTOs.Recommendation; -using API.DTOs.SeriesDetail; -using API.Entities; -using API.Entities.Enums; -using API.Entities.MetadataMatching; -using API.Extensions; -using API.Helpers; -using API.Middleware; -using API.Services; -using API.Services.Plus; using EasyCaching.Core; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; using Kavita.Common; using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Dashboard; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.DTOs.Metadata.Matching; +using Kavita.Models.DTOs.Recommendation; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.MetadataMatching; +using Kavita.Server.Attributes; +using Kavita.Server.Extensions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace API.Controllers; +namespace Kavita.Server.Controllers; -#nullable enable - -public class SeriesController : BaseApiController +public class SeriesController( + ILogger logger, + ITaskScheduler taskScheduler, + IUnitOfWork unitOfWork, + ISeriesService seriesService, + ILicenseService licenseService, + IEasyCachingProviderFactory cachingProviderFactory, + ILocalizationService localizationService, + IExternalMetadataService externalMetadataService, + IHostEnvironment environment) + : BaseApiController { - private readonly ILogger _logger; - private readonly ITaskScheduler _taskScheduler; - private readonly IUnitOfWork _unitOfWork; - private readonly ISeriesService _seriesService; - private readonly ILicenseService _licenseService; - private readonly ILocalizationService _localizationService; - private readonly IExternalMetadataService _externalMetadataService; - private readonly IHostEnvironment _environment; - private readonly IEasyCachingProvider _externalSeriesCacheProvider; - private readonly IEasyCachingProvider _matchSeriesCacheProvider; + private readonly IEasyCachingProvider _externalSeriesCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusExternalSeries); + private readonly IEasyCachingProvider _matchSeriesCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusMatchSeries); private const string CacheKey = "externalSeriesData_"; private const string MatchSeriesCacheKey = "matchSeries_"; - public SeriesController(ILogger logger, ITaskScheduler taskScheduler, IUnitOfWork unitOfWork, - ISeriesService seriesService, ILicenseService licenseService, - IEasyCachingProviderFactory cachingProviderFactory, ILocalizationService localizationService, - IExternalMetadataService externalMetadataService, IHostEnvironment environment) - { - _logger = logger; - _taskScheduler = taskScheduler; - _unitOfWork = unitOfWork; - _seriesService = seriesService; - _licenseService = licenseService; - _localizationService = localizationService; - _externalMetadataService = externalMetadataService; - _environment = environment; - - _externalSeriesCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusExternalSeries); - _matchSeriesCacheProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusMatchSeries); - } - /// /// Gets series with the applied Filter /// @@ -79,7 +58,7 @@ public class SeriesController : BaseApiController { var userId = UserId; var series = - await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, userParams, filterDto); + await unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, userParams, filterDto); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); @@ -92,10 +71,11 @@ public class SeriesController : BaseApiController /// Series Id to fetch details for /// /// Throws an exception if the series Id does exist + [SeriesAccess] [HttpGet("{seriesId:int}")] public async Task> GetSeries(int seriesId) { - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, UserId); + var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, UserId); if (series == null) return NoContent(); return Ok(series); } @@ -105,26 +85,31 @@ public class SeriesController : BaseApiController /// /// /// If the series was deleted or not - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpDelete("{seriesId}")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> DeleteSeries(int seriesId) { var username = Username!; - _logger.LogInformation("Series {SeriesId} is being deleted by {UserName}", seriesId, username); + logger.LogInformation("Series {SeriesId} is being deleted by {UserName}", seriesId, username); - return Ok(await _seriesService.DeleteMultipleSeries([seriesId])); + return Ok(await seriesService.DeleteMultipleSeries([seriesId])); } - [Authorize(Policy = PolicyGroups.AdminPolicy)] + /// + /// Deletes multiple series from Kavita at once + /// + /// + /// [HttpPost("delete-multiple")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task DeleteMultipleSeries(DeleteSeriesDto dto) { var username = Username!; - _logger.LogInformation("Series {@SeriesId} is being deleted by {UserName}", dto.SeriesIds, username); + logger.LogInformation("Series {@SeriesId} is being deleted by {UserName}", dto.SeriesIds, username); - if (await _seriesService.DeleteMultipleSeries(dto.SeriesIds)) return Ok(true); + if (await seriesService.DeleteMultipleSeries(dto.SeriesIds)) return Ok(true); - return BadRequest(await _localizationService.Translate(UserId, "generic-series-delete")); + return BadRequest(await localizationService.Translate(UserId, "generic-series-delete")); } /// @@ -132,24 +117,37 @@ public class SeriesController : BaseApiController /// /// /// + [SeriesAccess] [HttpGet("volumes")] public async Task>> GetVolumes(int seriesId) { - return Ok(await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, UserId)); + return Ok(await unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, UserId)); } + /// + /// Returns a single Volume with progress information and Chapters + /// + /// + /// + [VolumeAccess] [HttpGet("volume")] public async Task> GetVolume(int volumeId) { - var vol = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, UserId); + var vol = await unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, UserId); if (vol == null) return NoContent(); return Ok(vol); } + /// + /// Returns a single Chapter with progress information + /// + /// + /// + [ChapterAccess] [HttpGet("chapter")] public async Task> GetChapter(int chapterId) { - var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId, UserId); + var chapter = await unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId, UserId); if (chapter == null) return NoContent(); return Ok(chapter); @@ -161,11 +159,12 @@ public class SeriesController : BaseApiController /// /// Updated Series [HttpPost("update")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> UpdateSeries(UpdateSeriesDto updateSeries) { - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(updateSeries.Id); + var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(updateSeries.Id); if (series == null) - return BadRequest(await _localizationService.Translate(UserId, "series-doesnt-exist")); + return BadRequest(await localizationService.Translate(UserId, "series-doesnt-exist")); series.NormalizedName = series.Name.ToNormalized(); if (!string.IsNullOrEmpty(updateSeries.SortName?.Trim())) @@ -189,24 +188,24 @@ public class SeriesController : BaseApiController series.CoverImage = null; series.CoverImageLocked = false; series.Metadata.KPlusOverrides.Remove(MetadataSettingField.Covers); - _logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null: {SeriesId}", series.Id); + logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null: {SeriesId}", series.Id); series.ResetColorScape(); } - _unitOfWork.SeriesRepository.Update(series); + unitOfWork.SeriesRepository.Update(series); - if (!await _unitOfWork.CommitAsync()) + if (!await unitOfWork.CommitAsync()) { - return BadRequest(await _localizationService.Translate(UserId, "generic-series-update")); + return BadRequest(await localizationService.Translate(UserId, "generic-series-update")); } if (needsRefreshMetadata) { - await _taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id); + await taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id); } - return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(series.Id, UserId)); + return Ok(await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(series.Id, UserId)); } /// @@ -220,7 +219,7 @@ public class SeriesController : BaseApiController { var userId = UserId; var series = - await _unitOfWork.SeriesRepository.GetRecentlyAddedV2(userId, userParams, filterDto); + await unitOfWork.SeriesRepository.GetRecentlyAddedV2(userId, userParams, filterDto); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); @@ -236,7 +235,7 @@ public class SeriesController : BaseApiController public async Task>> GetRecentlyAddedChapters([FromQuery] UserParams? userParams) { userParams ??= UserParams.Default; - return Ok(await _unitOfWork.SeriesRepository.GetRecentlyUpdatedSeries(UserId, userParams)); + return Ok(await unitOfWork.SeriesRepository.GetRecentlyUpdatedSeries(UserId, userParams)); } /// @@ -255,10 +254,10 @@ public class SeriesController : BaseApiController { var seriesForUser = userId ?? UserId; - filterDto.Statements.AddRange(await _seriesService.GetProfilePrivacyStatements(seriesForUser, UserId)); + filterDto.Statements.AddRange(await seriesService.GetProfilePrivacyStatements(seriesForUser, UserId)); var series = - await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(seriesForUser, userParams, filterDto, context); + await unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(seriesForUser, userParams, filterDto, context); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); @@ -275,7 +274,7 @@ public class SeriesController : BaseApiController [HttpPost("on-deck")] public async Task>> GetOnDeck([FromQuery] UserParams userParams, [FromQuery] int libraryId = 0) { - var pagedList = await _unitOfWork.SeriesRepository.GetOnDeck(UserId, libraryId, userParams, null); + var pagedList = await unitOfWork.SeriesRepository.GetOnDeck(UserId, libraryId, userParams, null); Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages); @@ -291,7 +290,7 @@ public class SeriesController : BaseApiController [HttpPost("remove-from-on-deck")] public async Task RemoveFromOnDeck([FromQuery] int seriesId) { - await _unitOfWork.SeriesRepository.RemoveFromOnDeck(seriesId, UserId); + await unitOfWork.SeriesRepository.RemoveFromOnDeck(seriesId, UserId); return Ok(); } @@ -305,7 +304,7 @@ public class SeriesController : BaseApiController [HttpGet("currently-reading")] public async Task>> GetCurrentlyReadingForUser([FromQuery] UserParams userParams, [FromQuery] int userId) { - var pagedList = await _seriesService.GetCurrentlyReading(userId, UserId, userParams); + var pagedList = await seriesService.GetCurrentlyReading(userId, UserId, userParams); Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages); @@ -318,11 +317,11 @@ public class SeriesController : BaseApiController /// /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("refresh-metadata")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task RefreshSeriesMetadata(RefreshSeriesDto refreshSeriesDto) { - await _taskScheduler.RefreshSeriesMetadata(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate, refreshSeriesDto.ForceColorscape); + await taskScheduler.RefreshSeriesMetadata(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate, refreshSeriesDto.ForceColorscape); return Ok(); } @@ -331,11 +330,11 @@ public class SeriesController : BaseApiController /// /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("scan")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public ActionResult ScanSeries(RefreshSeriesDto refreshSeriesDto) { - _taskScheduler.ScanSeries(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, true); + taskScheduler.ScanSeries(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, true); return Ok(); } @@ -344,11 +343,11 @@ public class SeriesController : BaseApiController /// /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("analyze")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public ActionResult AnalyzeSeries(RefreshSeriesDto refreshSeriesDto) { - _taskScheduler.AnalyzeFilesForSeries(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate); + taskScheduler.AnalyzeFilesForSeries(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate); return Ok(); } @@ -357,10 +356,11 @@ public class SeriesController : BaseApiController /// /// /// + [SeriesAccess] [HttpGet("metadata")] public async Task> GetSeriesMetadata(int seriesId) { - return Ok(await _unitOfWork.SeriesRepository.GetSeriesMetadata(seriesId)); + return Ok(await unitOfWork.SeriesRepository.GetSeriesMetadata(seriesId)); } /// @@ -369,12 +369,13 @@ public class SeriesController : BaseApiController /// /// [HttpPost("metadata")] + [Authorize(PolicyGroups.AdminPolicy)] public async Task UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto) { - if (!await _seriesService.UpdateSeriesMetadata(updateSeriesMetadataDto)) - return BadRequest(await _localizationService.Translate(UserId, "update-metadata-fail")); + if (!await seriesService.UpdateSeriesMetadata(updateSeriesMetadataDto)) + return BadRequest(await localizationService.Translate(UserId, "update-metadata-fail")); - return Ok(await _localizationService.Translate(UserId, "series-updated")); + return Ok(await localizationService.Translate(UserId, "series-updated")); } @@ -389,7 +390,7 @@ public class SeriesController : BaseApiController { var userId = UserId; var series = - await _unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, userId, userParams); + await unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, userId, userParams); Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages); @@ -404,8 +405,8 @@ public class SeriesController : BaseApiController [HttpPost("series-by-ids")] public async Task>> GetAllSeriesById(SeriesByIdsDto dto) { - if (dto.SeriesIds == null) return BadRequest(await _localizationService.Translate(UserId, "invalid-payload")); - return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForIdsAsync(dto.SeriesIds, UserId)); + if (dto.SeriesIds == null) return BadRequest(await localizationService.Translate(UserId, "invalid-payload")); + return Ok(await unitOfWork.SeriesRepository.GetSeriesDtoForIdsAsync(dto.SeriesIds, UserId)); } /// @@ -413,13 +414,13 @@ public class SeriesController : BaseApiController /// /// /// - [ResponseCache(CacheProfileName = ResponseCacheProfiles.Month, VaryByQueryKeys = ["ageRating"])] [HttpGet("age-rating")] + [ResponseCache(CacheProfileName = ResponseCacheProfiles.Month, VaryByQueryKeys = ["ageRating"])] public async Task> GetAgeRating(int ageRating) { var val = (AgeRating) ageRating; if (val == AgeRating.NotApplicable) - return await _localizationService.Translate(UserId, "age-restriction-not-applicable"); + return await localizationService.Translate(UserId, "age-restriction-not-applicable"); return Ok(val.ToDescription()); } @@ -430,16 +431,17 @@ public class SeriesController : BaseApiController /// /// /// Do not rely on this API externally. May change without hesitation. + [SeriesAccess] [HttpGet("series-detail")] public async Task> GetSeriesDetailBreakdown(int seriesId) { try { - return await _seriesService.GetSeriesDetail(seriesId, UserId); + return await seriesService.GetSeriesDetail(seriesId, UserId); } catch (KavitaException ex) { - return BadRequest(await _localizationService.Translate(UserId, ex.Message)); + return BadRequest(await localizationService.Translate(UserId, ex.Message)); } } @@ -451,10 +453,11 @@ public class SeriesController : BaseApiController /// /// Type of Relationship to pull back /// + [SeriesAccess] [HttpGet("related")] public async Task>> GetRelatedSeries(int seriesId, RelationKind relation) { - return Ok(await _unitOfWork.SeriesRepository.GetSeriesForRelationKind(UserId, seriesId, relation)); + return Ok(await unitOfWork.SeriesRepository.GetSeriesForRelationKind(UserId, seriesId, relation)); } /// @@ -462,10 +465,11 @@ public class SeriesController : BaseApiController /// /// /// + [SeriesAccess] [HttpGet("all-related")] public async Task> GetAllRelatedSeries(int seriesId) { - return Ok(await _seriesService.GetRelatedSeries(UserId, seriesId)); + return Ok(await seriesService.GetRelatedSeries(UserId, seriesId)); } @@ -474,27 +478,23 @@ public class SeriesController : BaseApiController /// /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("update-related")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task UpdateRelatedSeries(UpdateRelatedSeriesDto dto) { - if (await _seriesService.UpdateRelatedSeries(dto)) + if (await seriesService.UpdateRelatedSeries(dto)) { return Ok(); } - return BadRequest(await _localizationService.Translate(UserId, "generic-relationship")); + return BadRequest(await localizationService.Translate(UserId, "generic-relationship")); } - [Authorize(Policy = PolicyGroups.AdminPolicy)] + [KPlus] [HttpGet("external-series-detail")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> GetExternalSeriesInfo(int? aniListId, long? malId, int? seriesId) { - if (!await _licenseService.HasActiveLicense()) - { - return BadRequest(); - } - var cacheKey = $"{CacheKey}-{aniListId ?? 0}-{malId ?? 0}-{seriesId ?? 0}"; var results = await _externalSeriesCacheProvider.GetAsync(cacheKey); if (results.HasValue) @@ -504,7 +504,7 @@ public class SeriesController : BaseApiController try { - var ret = await _externalMetadataService.GetExternalSeriesDetail(aniListId, malId, seriesId); + var ret = await externalMetadataService.GetExternalSeriesDetail(aniListId, malId, seriesId); await _externalSeriesCacheProvider.SetAsync(cacheKey, ret, TimeSpan.FromMinutes(15)); return Ok(ret); } @@ -520,12 +520,13 @@ public class SeriesController : BaseApiController /// /// /// + [SeriesAccess] [HttpGet("next-expected")] public async Task> GetNextExpectedChapter(int seriesId) { var userId = UserId; - return Ok(await _seriesService.GetEstimatedChapterCreationDate(seriesId, userId)); + return Ok(await seriesService.GetEstimatedChapterCreationDate(seriesId, userId)); } /// @@ -534,16 +535,17 @@ public class SeriesController : BaseApiController /// /// [HttpPost("match")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task>> MatchSeries(MatchSeriesDto dto) { var cacheKey = $"{MatchSeriesCacheKey}-{dto.SeriesId}-{dto.Query}"; var results = await _matchSeriesCacheProvider.GetAsync>(cacheKey); - if (results.HasValue && !_environment.IsDevelopment()) + if (results.HasValue && !environment.IsDevelopment()) { return Ok(results.Value); } - var ret = await _externalMetadataService.MatchSeries(dto); + var ret = await externalMetadataService.MatchSeries(dto); await _matchSeriesCacheProvider.SetAsync(cacheKey, ret, TimeSpan.FromMinutes(1)); return Ok(ret); @@ -556,9 +558,10 @@ public class SeriesController : BaseApiController /// /// [HttpPost("update-match")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public ActionResult UpdateSeriesMatch([FromQuery] int seriesId, [FromQuery] int? aniListId, [FromQuery] long? malId, [FromQuery] int? cbrId) { - BackgroundJob.Enqueue(() => _externalMetadataService.FixSeriesMatch(seriesId, aniListId, malId, cbrId)); + BackgroundJob.Enqueue(() => externalMetadataService.FixSeriesMatch(seriesId, aniListId, malId, cbrId)); return Ok(); } @@ -570,9 +573,10 @@ public class SeriesController : BaseApiController /// /// [HttpPost("dont-match")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task UpdateDontMatch([FromQuery] int seriesId, [FromQuery] bool dontMatch) { - await _externalMetadataService.UpdateSeriesDontMatch(seriesId, dontMatch); + await externalMetadataService.UpdateSeriesDontMatch(seriesId, dontMatch); return Ok(); } @@ -583,7 +587,7 @@ public class SeriesController : BaseApiController [HttpGet("series-with-annotations")] public async Task>> GetSeriesWithAnnotations() { - var data = await _unitOfWork.AnnotationRepository.GetSeriesWithAnnotations(UserId); + var data = await unitOfWork.AnnotationRepository.GetSeriesWithAnnotations(UserId); return Ok(data); } diff --git a/API/Controllers/ServerController.cs b/Kavita.Server/Controllers/ServerController.cs similarity index 61% rename from API/Controllers/ServerController.cs rename to Kavita.Server/Controllers/ServerController.cs index 9bd3bbba9..5dc8e033f 100644 --- a/API/Controllers/ServerController.cs +++ b/Kavita.Server/Controllers/ServerController.cs @@ -3,64 +3,45 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.Jobs; -using API.DTOs.MediaErrors; -using API.DTOs.Stats; -using API.DTOs.Update; -using API.Entities.Enums; -using API.Helpers; -using API.Services; -using API.Services.Tasks; using EasyCaching.Core; using Hangfire; using Hangfire.Storage; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Scanner; using Kavita.Common; +using Kavita.Common.Helpers; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Jobs; +using Kavita.Models.DTOs.MediaErrors; +using Kavita.Models.DTOs.Stats; +using Kavita.Models.DTOs.Update; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Scanner; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using MimeTypes; -using TaskScheduler = API.Services.TaskScheduler; +using TaskScheduler = Kavita.Services.TaskScheduler; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; [Authorize(PolicyGroups.AdminPolicy)] -public class ServerController : BaseApiController +public class ServerController( + ILogger logger, + IBackupService backupService, + IArchiveService archiveService, + IVersionUpdaterService versionUpdaterService, + IStatsService statsService, + ICleanupService cleanupService, + IScannerService scannerService, + ITaskScheduler taskScheduler, + IUnitOfWork unitOfWork, + IEasyCachingProviderFactory cachingProviderFactory, + IThemeService themeService, + ILocalizationService localizationService) + : BaseApiController { - private readonly ILogger _logger; - private readonly IBackupService _backupService; - private readonly IArchiveService _archiveService; - private readonly IVersionUpdaterService _versionUpdaterService; - private readonly IStatsService _statsService; - private readonly ICleanupService _cleanupService; - private readonly IScannerService _scannerService; - private readonly ITaskScheduler _taskScheduler; - private readonly IUnitOfWork _unitOfWork; - private readonly IEasyCachingProviderFactory _cachingProviderFactory; - private readonly ILocalizationService _localizationService; - - public ServerController(ILogger logger, - IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, - IStatsService statsService, ICleanupService cleanupService, IScannerService scannerService, - ITaskScheduler taskScheduler, IUnitOfWork unitOfWork, IEasyCachingProviderFactory cachingProviderFactory, - ILocalizationService localizationService) - { - _logger = logger; - _backupService = backupService; - _archiveService = archiveService; - _versionUpdaterService = versionUpdaterService; - _statsService = statsService; - _cleanupService = cleanupService; - _scannerService = scannerService; - _taskScheduler = taskScheduler; - _unitOfWork = unitOfWork; - _cachingProviderFactory = cachingProviderFactory; - _localizationService = localizationService; - } - /// /// Performs an ad-hoc cleanup of Cache /// @@ -68,8 +49,8 @@ public class ServerController : BaseApiController [HttpPost("clear-cache")] public ActionResult ClearCache() { - _logger.LogInformation("{UserName} is clearing cache of server from admin dashboard", Username!); - _cleanupService.CleanupCacheAndTempDirectories(); + logger.LogInformation("{UserName} is clearing cache of server from admin dashboard", Username!); + cleanupService.CleanupCacheAndTempDirectories(); return Ok(); } @@ -81,7 +62,7 @@ public class ServerController : BaseApiController [HttpPost("cleanup-want-to-read")] public ActionResult CleanupWantToRead() { - _logger.LogInformation("{UserName} is clearing running want to read cleanup from admin dashboard", Username!); + logger.LogInformation("{UserName} is clearing running want to read cleanup from admin dashboard", Username!); RecurringJob.TriggerJob(TaskScheduler.RemoveFromWantToReadTaskId); return Ok(); @@ -94,7 +75,7 @@ public class ServerController : BaseApiController [HttpPost("cleanup")] public ActionResult Cleanup() { - _logger.LogInformation("{UserName} is clearing running general cleanup from admin dashboard", Username!); + logger.LogInformation("{UserName} is clearing running general cleanup from admin dashboard", Username!); RecurringJob.TriggerJob(TaskScheduler.CleanupTaskId); return Ok(); @@ -107,7 +88,7 @@ public class ServerController : BaseApiController [HttpPost("backup-db")] public ActionResult BackupDatabase() { - _logger.LogInformation("{UserName} is backing up database of server from admin dashboard", Username!); + logger.LogInformation("{UserName} is backing up database of server from admin dashboard", Username!); RecurringJob.TriggerJob(TaskScheduler.BackupTaskId); return Ok(); } @@ -119,12 +100,12 @@ public class ServerController : BaseApiController [HttpPost("analyze-files")] public async Task AnalyzeFiles() { - _logger.LogInformation("{UserName} is performing file analysis from admin dashboard", Username!); + logger.LogInformation("{UserName} is performing file analysis from admin dashboard", Username!); if (TaskScheduler.HasAlreadyEnqueuedTask(ScannerService.Name, "AnalyzeFiles", [], TaskScheduler.DefaultQueue, true)) - return Ok(await _localizationService.Translate(UserId, "job-already-running")); + return Ok(await localizationService.Translate(UserId, "job-already-running")); - BackgroundJob.Enqueue(() => _scannerService.AnalyzeFiles()); + BackgroundJob.Enqueue(() => scannerService.AnalyzeFiles()); return Ok(); } @@ -137,7 +118,7 @@ public class ServerController : BaseApiController [HttpGet("server-info-slim")] public async Task> GetSlimVersion() { - return Ok(await _statsService.GetServerInfoSlim()); + return Ok(await statsService.GetServerInfoSlim()); } @@ -148,13 +129,13 @@ public class ServerController : BaseApiController [HttpPost("convert-media")] public async Task ScheduleConvertCovers() { - var encoding = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; + var encoding = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; if (encoding == EncodeFormat.PNG) { - return BadRequest(await _localizationService.Translate(UserId, "encode-as-warning")); + return BadRequest(await localizationService.Translate(UserId, "encode-as-warning")); } - _taskScheduler.CovertAllCoversToEncoding(); + taskScheduler.ConvertAllCoversToEncoding(); return Ok(); } @@ -166,16 +147,16 @@ public class ServerController : BaseApiController [HttpGet("logs")] public async Task GetLogs() { - var files = _backupService.GetLogFiles(); + var files = backupService.GetLogFiles(); try { - var zipPath = _archiveService.CreateZipForDownload(files, "logs"); + var zipPath = archiveService.CreateZipForDownload(files, "logs"); return PhysicalFile(zipPath, MimeTypeMap.GetMimeType(Path.GetExtension(zipPath)), System.Web.HttpUtility.UrlEncode(Path.GetFileName(zipPath)), true); } catch (KavitaException ex) { - return BadRequest(await _localizationService.Translate(UserId, ex.Message)); + return BadRequest(await localizationService.Translate(UserId, ex.Message)); } } @@ -186,7 +167,7 @@ public class ServerController : BaseApiController [HttpGet("check-for-updates")] public async Task CheckForAnnouncements() { - await _taskScheduler.CheckForUpdate(); + await taskScheduler.CheckForUpdate(); return Ok(); } @@ -196,7 +177,7 @@ public class ServerController : BaseApiController [HttpGet("check-update")] public async Task> CheckForUpdates() { - return Ok(await _versionUpdaterService.CheckForUpdate()); + return Ok(await versionUpdaterService.CheckForUpdate()); } /// @@ -206,7 +187,7 @@ public class ServerController : BaseApiController [HttpGet("check-out-of-date")] public async Task> CheckHowOutOfDate(bool stableOnly = true) { - return Ok(await _versionUpdaterService.GetNumberOfReleasesBehind(stableOnly)); + return Ok(await versionUpdaterService.GetNumberOfReleasesBehind(stableOnly)); } @@ -215,14 +196,10 @@ public class ServerController : BaseApiController /// /// How many releases from the latest to return /// - [AllowAnonymous] [HttpGet("changelog")] public async Task>> GetChangelog(int count = 0) { - // Strange bug where [Authorize] doesn't work - if (UserId == 0) return Unauthorized(); - - return Ok(await _versionUpdaterService.GetAllReleases(count)); + return Ok(await versionUpdaterService.GetAllReleases(count)); } /// @@ -236,7 +213,7 @@ public class ServerController : BaseApiController new JobDto() { Id = dto.Id, - Title = await _localizationService.Translate(UserId, dto.Id), + Title = await localizationService.Translate(UserId, dto.Id), Cron = dto.Cron, LastExecutionUtc = dto.LastExecution.HasValue ? new DateTime(dto.LastExecution.Value.Ticks, DateTimeKind.Utc) : null }); @@ -252,7 +229,7 @@ public class ServerController : BaseApiController [HttpGet("media-errors")] public ActionResult> GetMediaErrors() { - return Ok(_unitOfWork.MediaErrorRepository.GetAllErrorDtosAsync()); + return Ok(unitOfWork.MediaErrorRepository.GetAllErrorDtosAsync()); } /// @@ -263,7 +240,7 @@ public class ServerController : BaseApiController [HttpPost("clear-media-alerts")] public async Task ClearMediaErrors() { - await _unitOfWork.MediaErrorRepository.DeleteAll(); + await unitOfWork.MediaErrorRepository.DeleteAll(); return Ok(); } @@ -276,8 +253,8 @@ public class ServerController : BaseApiController [HttpPost("bust-kavitaplus-cache")] public async Task BustReviewAndRecCache() { - _logger.LogInformation("Busting Kavita+ Cache"); - var provider = _cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusExternalSeries); + logger.LogInformation("Busting Kavita+ Cache"); + var provider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.KavitaPlusExternalSeries); await provider.FlushAsync(); return Ok(); } @@ -290,7 +267,7 @@ public class ServerController : BaseApiController [HttpPost("sync-themes")] public async Task SyncThemes() { - await _taskScheduler.SyncThemes(); + await themeService.SyncThemes(); return Ok(); } diff --git a/API/Controllers/SettingsController.cs b/Kavita.Server/Controllers/SettingsController.cs similarity index 62% rename from API/Controllers/SettingsController.cs rename to Kavita.Server/Controllers/SettingsController.cs index 50f718d55..8f88345c5 100644 --- a/API/Controllers/SettingsController.cs +++ b/Kavita.Server/Controllers/SettingsController.cs @@ -2,54 +2,39 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs; -using API.DTOs.Email; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Settings; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers.Converters; -using API.Services; using AutoMapper; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; using Kavita.Common.Extensions; using Kavita.Common.Helpers; +using Kavita.Models; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Email; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.Entities.Enums; +using Kavita.Server.Extensions; +using Kavita.Services; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace API.Controllers; +namespace Kavita.Server.Controllers; -#nullable enable - -public class SettingsController : BaseApiController +public class SettingsController( + ILogger logger, + IUnitOfWork unitOfWork, + IMapper mapper, + IEmailService emailService, + ILocalizationService localizationService, + ISettingsService settingsService, + IAuthenticationSchemeProvider authenticationSchemeProvider, + IOidcService oidcService) + : BaseApiController { - private readonly ILogger _logger; - private readonly IUnitOfWork _unitOfWork; - private readonly IMapper _mapper; - private readonly IEmailService _emailService; - private readonly ILocalizationService _localizationService; - private readonly ISettingsService _settingsService; - private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider; - private readonly IOidcService _oidcService; - - public SettingsController(ILogger logger, IUnitOfWork unitOfWork, IMapper mapper, - IEmailService emailService, ILocalizationService localizationService, ISettingsService settingsService, - IAuthenticationSchemeProvider authenticationSchemeProvider, IOidcService oidcService) - { - _logger = logger; - _unitOfWork = unitOfWork; - _mapper = mapper; - _emailService = emailService; - _localizationService = localizationService; - _settingsService = settingsService; - _authenticationSchemeProvider = authenticationSchemeProvider; - _oidcService = oidcService; - } - /// /// Returns the base url for this instance (if set) /// @@ -57,7 +42,7 @@ public class SettingsController : BaseApiController [HttpGet("base-url")] public async Task> GetBaseUrl() { - var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var settingsDto = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); return Ok(settingsDto.BaseUrl); } @@ -65,67 +50,67 @@ public class SettingsController : BaseApiController /// Returns the server settings /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpGet] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> GetSettings() { - var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var settingsDto = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); // Do not send OIDC secret to user settingsDto.OidcConfig.Secret = "*".Repeat(settingsDto.OidcConfig.Secret.Length); return Ok(settingsDto); } - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("reset")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> ResetSettings() { - _logger.LogInformation("{UserName} is resetting Server Settings", Username!); + logger.LogInformation("{UserName} is resetting Server Settings", Username!); - return await UpdateSettings(_mapper.Map(Seed.DefaultSettings)); + return await UpdateSettings(mapper.Map(Defaults.DefaultSettings)); } /// /// Resets the IP Addresses /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("reset-ip-addresses")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> ResetIpAddressesSettings() { - _logger.LogInformation("{UserName} is resetting IP Addresses Setting", Username!); - var ipAddresses = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.IpAddresses); + logger.LogInformation("{UserName} is resetting IP Addresses Setting", Username!); + var ipAddresses = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.IpAddresses); ipAddresses.Value = Configuration.DefaultIpAddresses; - _unitOfWork.SettingsRepository.Update(ipAddresses); + unitOfWork.SettingsRepository.Update(ipAddresses); - if (!await _unitOfWork.CommitAsync()) + if (!await unitOfWork.CommitAsync()) { - await _unitOfWork.RollbackAsync(); + await unitOfWork.RollbackAsync(); } - return Ok(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()); + return Ok(await unitOfWork.SettingsRepository.GetSettingsDtoAsync()); } /// /// Resets the Base url /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("reset-base-url")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> ResetBaseUrlSettings() { - _logger.LogInformation("{UserName} is resetting Base Url Setting", Username!); - var baseUrl = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BaseUrl); + logger.LogInformation("{UserName} is resetting Base Url Setting", Username!); + var baseUrl = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BaseUrl); baseUrl.Value = Configuration.DefaultBaseUrl; - _unitOfWork.SettingsRepository.Update(baseUrl); + unitOfWork.SettingsRepository.Update(baseUrl); - if (!await _unitOfWork.CommitAsync()) + if (!await unitOfWork.CommitAsync()) { - await _unitOfWork.RollbackAsync(); + await unitOfWork.RollbackAsync(); } Configuration.BaseUrl = baseUrl.Value; - return Ok(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()); + return Ok(await unitOfWork.SettingsRepository.GetSettingsDtoAsync()); } /// @@ -135,7 +120,7 @@ public class SettingsController : BaseApiController [HttpGet("is-email-setup")] public async Task> IsEmailSetup() { - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); return Ok(settings.IsEmailSetup()); } @@ -145,25 +130,25 @@ public class SettingsController : BaseApiController /// /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> UpdateSettings(ServerSettingDto updateSettingsDto) { - _logger.LogInformation("{UserName} is updating Server Settings", Username!); + logger.LogInformation("{UserName} is updating Server Settings", Username!); try { - var d = await _settingsService.UpdateSettings(updateSettingsDto); + var d = await settingsService.UpdateSettings(updateSettingsDto); return Ok(d); } catch (KavitaException ex) { - return BadRequest(await _localizationService.Translate(UserId, ex.Message)); + return BadRequest(await localizationService.Translate(UserId, ex.Message)); } catch (Exception ex) { - _logger.LogError(ex, "There was an exception when updating server settings"); - return BadRequest(await _localizationService.Translate(UserId, "generic-error")); + logger.LogError(ex, "There was an exception when updating server settings"); + return BadRequest(await localizationService.Translate(UserId, "generic-error")); } } @@ -171,22 +156,22 @@ public class SettingsController : BaseApiController /// All values allowed for Task Scheduling APIs. A custom cron job is not included. Disabled is not applicable for Cleanup. /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpGet("task-frequencies")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public ActionResult> GetTaskFrequencies() { return Ok(CronConverter.Options); } - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpGet("library-types")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public ActionResult> GetLibraryTypes() { return Ok(Enum.GetValues().Select(t => t.ToDescription())); } - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpGet("log-levels")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public ActionResult> GetLogLevels() { return Ok(new[] {"Trace", "Debug", "Information", "Warning", "Critical"}); @@ -195,7 +180,7 @@ public class SettingsController : BaseApiController [HttpGet("opds-enabled")] public async Task> GetOpdsEnabled() { - var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var settingsDto = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); return Ok(settingsDto.EnableOpds); } @@ -215,24 +200,24 @@ public class SettingsController : BaseApiController /// Sends a test email to see if email settings are hooked up correctly /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("test-email-url")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> TestEmailServiceUrl() { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(UserId); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId); if (string.IsNullOrEmpty(user?.Email)) return BadRequest("Your account has no email on record. Cannot email."); - return Ok(await _emailService.SendTestEmail(user!.Email)); + return Ok(await emailService.SendTestEmail(user!.Email)); } /// /// Get the metadata settings for Kavita+ users. /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpGet("metadata-settings")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> GetMetadataSettings() { - return Ok(await _unitOfWork.SettingsRepository.GetMetadataSettingDto()); + return Ok(await unitOfWork.SettingsRepository.GetMetadataSettingDto()); } @@ -241,17 +226,17 @@ public class SettingsController : BaseApiController /// /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("metadata-settings")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> UpdateMetadataSettings(MetadataSettingsDto dto) { try { - return Ok(await _settingsService.UpdateMetadataSettings(dto)); + return Ok(await settingsService.UpdateMetadataSettings(dto)); } catch (Exception ex) { - _logger.LogError(ex, "There was an issue when updating metadata settings"); + logger.LogError(ex, "There was an issue when updating metadata settings"); return BadRequest(ex.Message); } } @@ -260,17 +245,17 @@ public class SettingsController : BaseApiController /// Import field mappings /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpPost("import-field-mappings")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> ImportFieldMappings([FromBody] ImportFieldMappingsDto dto) { try { - return Ok(await _settingsService.ImportFieldMappings(dto.Data, dto.Settings)); + return Ok(await settingsService.ImportFieldMappings(dto.Data, dto.Settings)); } catch (Exception ex) { - _logger.LogError(ex, "There was an issue importing field mappings"); + logger.LogError(ex, "There was an issue importing field mappings"); return BadRequest(ex.Message); } } @@ -284,10 +269,10 @@ public class SettingsController : BaseApiController [HttpGet("oidc")] public async Task> GetOidcConfig() { - var oidcScheme = await _authenticationSchemeProvider.GetSchemeAsync(IdentityServiceExtensions.OpenIdConnect); + var oidcScheme = await authenticationSchemeProvider.GetSchemeAsync(IdentityServiceExtensions.OpenIdConnect); - var settings = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; - var publicConfig = _mapper.Map(settings); + var settings = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; + var publicConfig = mapper.Map(settings); publicConfig.Enabled = oidcScheme != null && !string.IsNullOrEmpty(settings.Authority) && !string.IsNullOrEmpty(settings.ClientId) && @@ -300,7 +285,7 @@ public class SettingsController : BaseApiController [HttpPost("reset-external-ids")] public async Task ResetExternalIds() { - await _oidcService.ClearOidcIds(); + await oidcService.ClearOidcIds(); return Ok(); } @@ -314,7 +299,7 @@ public class SettingsController : BaseApiController [HttpPost("is-valid-authority")] public async Task> IsValidAuthority([FromBody] AuthorityValidationDto authority) { - return Ok(await _settingsService.IsValidAuthority(authority.Authority)); + return Ok(await settingsService.IsValidAuthority(authority.Authority)); } diff --git a/API/Controllers/StatsController.cs b/Kavita.Server/Controllers/StatsController.cs similarity index 93% rename from API/Controllers/StatsController.cs rename to Kavita.Server/Controllers/StatsController.cs index c996e741c..69f4e79fe 100644 --- a/API/Controllers/StatsController.cs +++ b/Kavita.Server/Controllers/StatsController.cs @@ -5,31 +5,27 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Metadata; -using API.DTOs.Person; -using API.DTOs.Statistics; -using API.DTOs.Stats.V3; -using API.DTOs.Stats.V3.ClientDevice; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers; -using API.Middleware; -using API.Services; -using API.Services.Tasks.Scanner.Parser; using CsvHelper; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.Statistics; +using Kavita.Models.DTOs.Stats.V3.ClientDevice; +using Kavita.Models.Entities.Enums; +using Kavita.Server.Attributes; +using Kavita.Server.Extensions; +using Kavita.Services.Scanner; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using MimeTypes; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; public class StatsController( IStatisticService statService, @@ -308,13 +304,13 @@ public class StatsController( { await CleanStatsFilter(filter, UserId); - return Ok(await statService.GetReadingPaceForUser(filter, userId, year, booksOnly, UserId)); + return Ok(await statService.GetReadingPaceForUser(filter, userId, year, booksOnly, UserId, HttpContext.RequestAborted)); } /// - /// Returns top 10 genres that user likes reading + /// Returns the top 10 genres that the user likes reading /// /// /// @@ -326,7 +322,7 @@ public class StatsController( { await CleanStatsFilter(filter, UserId); - return Ok(await statService.GetGenreBreakdownForUser(filter, userId, UserId)); + return Ok(await statService.GetGenreBreakdownForUser(filter, userId, UserId, HttpContext.RequestAborted)); } /// @@ -342,7 +338,7 @@ public class StatsController( { await CleanStatsFilter(filter, UserId); - return Ok(await statService.GetTagBreakdownForUser(filter, userId, UserId)); + return Ok(await statService.GetTagBreakdownForUser(filter, userId, UserId, HttpContext.RequestAborted)); } @@ -353,7 +349,7 @@ public class StatsController( { await CleanStatsFilter(filter, UserId); - return Ok(await statService.GetPageSpreadForUser(filter, userId, UserId)); + return Ok(await statService.GetPageSpreadForUser(filter, userId, UserId, HttpContext.RequestAborted)); } [ProfilePrivacy] @@ -363,7 +359,7 @@ public class StatsController( { await CleanStatsFilter(filter, UserId); - return Ok(await statService.GetWordSpreadForUser(filter, userId, UserId)); + return Ok(await statService.GetWordSpreadForUser(filter, userId, UserId, HttpContext.RequestAborted)); } [ProfilePrivacy] @@ -373,7 +369,7 @@ public class StatsController( { await CleanStatsFilter(filter, UserId); - return Ok(await statService.GetMostReadAuthors(filter, userId, UserId)); + return Ok(await statService.GetMostReadAuthors(filter, userId, UserId, HttpContext.RequestAborted)); } /// @@ -389,7 +385,7 @@ public class StatsController( { await CleanStatsFilter(filter, UserId); - var dto = await statService.GetTimeReadingByHour(filter, userId, UserId); + var dto = await statService.GetTimeReadingByHour(filter, userId, UserId, HttpContext.RequestAborted); if (dto == null) return BadRequest(); return Ok(dto); @@ -408,7 +404,7 @@ public class StatsController( { await CleanStatsFilter(filter, UserId); - return Ok(await statService.GetReadsPerMonth(filter, userId, UserId)); + return Ok(await statService.GetReadsPerMonth(filter, userId, UserId, HttpContext.RequestAborted)); } /// @@ -421,7 +417,7 @@ public class StatsController( [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute)] public async Task> GetTotalReads(int userId) { - return Ok(await statService.GetTotalReads(userId, UserId)); + return Ok(await statService.GetTotalReads(userId, UserId, HttpContext.RequestAborted)); } [ProfilePrivacy] @@ -429,7 +425,7 @@ public class StatsController( public async Task> GetStatsForUserBar([FromQuery] StatsFilterDto filter, int userId) { await CleanStatsFilter(filter, userId); - return Ok(await statService.GetUserStatBar(filter, userId, UserId)); + return Ok(await statService.GetUserStatBar(filter, userId, UserId, HttpContext.RequestAborted)); } [ProfilePrivacy] @@ -437,7 +433,7 @@ public class StatsController( [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute)] public async Task> GetUserReadStatistics(int userId) { - return Ok(await statService.GetUserReadStatistics(userId, [])); + return Ok(await statService.GetUserReadStatistics(userId, [], HttpContext.RequestAborted)); } @@ -451,7 +447,7 @@ public class StatsController( [HttpGet("reading-history")] public async Task>> GetReadingHistoryItems([FromQuery] StatsFilterDto filter, [FromQuery] UserParams userParams) { - var result = await statService.GetReadingHistoryItems(filter, userParams, UserId, UserId); + var result = await statService.GetReadingHistoryItems(filter, userParams, UserId, UserId, HttpContext.RequestAborted); Response.AddPaginationHeader(result.CurrentPage, result.PageSize, result.TotalCount, result.TotalPages); diff --git a/API/Controllers/StreamController.cs b/Kavita.Server/Controllers/StreamController.cs similarity index 75% rename from API/Controllers/StreamController.cs rename to Kavita.Server/Controllers/StreamController.cs index 35dab90c8..fdb6834d9 100644 --- a/API/Controllers/StreamController.cs +++ b/Kavita.Server/Controllers/StreamController.cs @@ -1,33 +1,23 @@ using System.Collections.Generic; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.Dashboard; -using API.DTOs.SideNav; -using API.Middleware; -using API.Services; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Dashboard; +using Kavita.Models.DTOs.SideNav; +using Kavita.Server.Attributes; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; - -#nullable enable - +namespace Kavita.Server.Controllers; /// /// Responsible for anything that deals with Streams (SmartFilters, ExternalSource, DashboardStream, SideNavStream) /// -public class StreamController : BaseApiController +public class StreamController( + IStreamService streamService, + IUnitOfWork unitOfWork) + : BaseApiController { - private readonly IStreamService _streamService; - private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; - - public StreamController(IStreamService streamService, IUnitOfWork unitOfWork, ILocalizationService localizationService) - { - _streamService = streamService; - _unitOfWork = unitOfWork; - _localizationService = localizationService; - } - /// /// Returns the layout of the user's dashboard /// @@ -35,7 +25,7 @@ public class StreamController : BaseApiController [HttpGet("dashboard")] public async Task>> GetDashboardLayout(bool visibleOnly = true) { - return Ok(await _streamService.GetDashboardStreams(UserId, visibleOnly)); + return Ok(await streamService.GetDashboardStreams(UserId, visibleOnly)); } /// @@ -44,7 +34,7 @@ public class StreamController : BaseApiController [HttpGet("sidenav")] public async Task>> GetSideNav(bool visibleOnly = true) { - return Ok(await _streamService.GetSidenavStreams(UserId, visibleOnly)); + return Ok(await streamService.GetSidenavStreams(UserId, visibleOnly)); } /// @@ -53,7 +43,7 @@ public class StreamController : BaseApiController [HttpGet("external-sources")] public async Task>> GetExternalSources() { - return Ok(await _streamService.GetExternalSources(UserId)); + return Ok(await streamService.GetExternalSources(UserId)); } /// @@ -65,7 +55,7 @@ public class StreamController : BaseApiController public async Task> CreateExternalSource(ExternalSourceDto dto) { // Check if a host and api key exists for the current user - return Ok(await _streamService.CreateExternalSource(UserId, dto)); + return Ok(await streamService.CreateExternalSource(UserId, dto)); } /// @@ -78,7 +68,7 @@ public class StreamController : BaseApiController public async Task> UpdateExternalSource(ExternalSourceDto dto) { // Check if a host and api key exists for the current user - return Ok(await _streamService.UpdateExternalSource(UserId, dto)); + return Ok(await streamService.UpdateExternalSource(UserId, dto)); } /// @@ -90,7 +80,7 @@ public class StreamController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> ExternalSourceExists(ExternalSourceDto dto) { - return Ok(await _unitOfWork.AppUserExternalSourceRepository.ExternalSourceExists(UserId, dto.Name, dto.Host, dto.ApiKey)); + return Ok(await unitOfWork.AppUserExternalSourceRepository.ExternalSourceExists(UserId, dto.Name, dto.Host, dto.ApiKey)); } /// @@ -102,7 +92,7 @@ public class StreamController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task ExternalSourceExists(int externalSourceId) { - await _streamService.DeleteExternalSource(UserId, externalSourceId); + await streamService.DeleteExternalSource(UserId, externalSourceId); return Ok(); } @@ -116,7 +106,7 @@ public class StreamController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> AddDashboard([FromQuery] int smartFilterId) { - return Ok(await _streamService.CreateDashboardStreamFromSmartFilter(UserId, smartFilterId)); + return Ok(await streamService.CreateDashboardStreamFromSmartFilter(UserId, smartFilterId)); } /// @@ -128,7 +118,7 @@ public class StreamController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task UpdateDashboardStream(DashboardStreamDto dto) { - await _streamService.UpdateDashboardStream(UserId, dto); + await streamService.UpdateDashboardStream(UserId, dto); return Ok(); } @@ -141,7 +131,7 @@ public class StreamController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task UpdateDashboardStreamPosition(UpdateStreamPositionDto dto) { - await _streamService.UpdateDashboardStreamPosition(UserId, dto); + await streamService.UpdateDashboardStreamPosition(UserId, dto); return Ok(); } @@ -155,7 +145,7 @@ public class StreamController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> AddSideNav([FromQuery] int smartFilterId) { - return Ok(await _streamService.CreateSideNavStreamFromSmartFilter(UserId, smartFilterId)); + return Ok(await streamService.CreateSideNavStreamFromSmartFilter(UserId, smartFilterId)); } /// @@ -167,7 +157,7 @@ public class StreamController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> AddSideNavFromExternalSource([FromQuery] int externalSourceId) { - return Ok(await _streamService.CreateSideNavStreamFromExternalSource(UserId, externalSourceId)); + return Ok(await streamService.CreateSideNavStreamFromExternalSource(UserId, externalSourceId)); } /// @@ -179,7 +169,7 @@ public class StreamController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task UpdateSideNavStream(SideNavStreamDto dto) { - await _streamService.UpdateSideNavStream(UserId, dto); + await streamService.UpdateSideNavStream(UserId, dto); return Ok(); } @@ -192,7 +182,7 @@ public class StreamController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task UpdateSideNavStreamPosition(UpdateStreamPositionDto dto) { - await _streamService.UpdateSideNavStreamPosition(UserId, dto); + await streamService.UpdateSideNavStreamPosition(UserId, dto); return Ok(); } @@ -200,7 +190,7 @@ public class StreamController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task BulkUpdateSideNavStream(BulkUpdateSideNavStreamVisibilityDto dto) { - await _streamService.UpdateSideNavStreamBulk(UserId, dto); + await streamService.UpdateSideNavStreamBulk(UserId, dto); return Ok(); } @@ -213,7 +203,7 @@ public class StreamController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task DeleteSmartFilterSideNavStream([FromQuery] int sideNavStreamId) { - await _streamService.DeleteSideNavSmartFilterStream(UserId, sideNavStreamId); + await streamService.DeleteSideNavSmartFilterStream(UserId, sideNavStreamId); return Ok(); } @@ -226,7 +216,7 @@ public class StreamController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task DeleteSmartFilterDashboardStream([FromQuery] int dashboardStreamId) { - await _streamService.DeleteDashboardSmartFilterStream(UserId, dashboardStreamId); + await streamService.DeleteDashboardSmartFilterStream(UserId, dashboardStreamId); return Ok(); } } diff --git a/API/Controllers/TachiyomiController.cs b/Kavita.Server/Controllers/TachiyomiController.cs similarity index 53% rename from API/Controllers/TachiyomiController.cs rename to Kavita.Server/Controllers/TachiyomiController.cs index c370f6169..eac5d2dec 100644 --- a/API/Controllers/TachiyomiController.cs +++ b/Kavita.Server/Controllers/TachiyomiController.cs @@ -1,32 +1,22 @@ using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.Services; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.Models.DTOs; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; /// /// All APIs are for Tachiyomi extension and app. They have hacks for our implementation and should not be used for any /// other purposes. /// -public class TachiyomiController : BaseApiController +public class TachiyomiController( + IUnitOfWork unitOfWork, + ITachiyomiService tachiyomiService, + ILocalizationService localizationService) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly ITachiyomiService _tachiyomiService; - private readonly ILocalizationService _localizationService; - - public TachiyomiController(IUnitOfWork unitOfWork, ITachiyomiService tachiyomiService, - ILocalizationService localizationService) - { - _unitOfWork = unitOfWork; - _tachiyomiService = tachiyomiService; - _localizationService = localizationService; - } - /// /// Given the series Id, this should return the latest chapter that has been fully read. /// @@ -35,8 +25,8 @@ public class TachiyomiController : BaseApiController [HttpGet("latest-chapter")] public async Task> GetLatestChapter(int seriesId) { - if (seriesId < 1) return BadRequest(await _localizationService.Translate(UserId, "greater-0", "SeriesId")); - return Ok(await _tachiyomiService.GetLatestChapter(seriesId, UserId)); + if (seriesId < 1) return BadRequest(await localizationService.Translate(UserId, "greater-0", "SeriesId")); + return Ok(await tachiyomiService.GetLatestChapter(seriesId, UserId)); } /// @@ -47,8 +37,8 @@ public class TachiyomiController : BaseApiController [HttpPost("mark-chapter-until-as-read")] public async Task> MarkChaptersUntilAsRead(int seriesId, float chapterNumber) { - var user = (await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, + var user = (await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.Progress))!; - return Ok(await _tachiyomiService.MarkChaptersUntilAsRead(user, seriesId, chapterNumber)); + return Ok(await tachiyomiService.MarkChaptersUntilAsRead(user, seriesId, chapterNumber)); } } diff --git a/API/Controllers/ThemeController.cs b/Kavita.Server/Controllers/ThemeController.cs similarity index 63% rename from API/Controllers/ThemeController.cs rename to Kavita.Server/Controllers/ThemeController.cs index 1efc969a4..a970fafe5 100644 --- a/API/Controllers/ThemeController.cs +++ b/Kavita.Server/Controllers/ThemeController.cs @@ -2,45 +2,32 @@ using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.Theme; -using API.Middleware; -using API.Services; -using API.Services.Tasks; using AutoMapper; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Theme; +using Kavita.Server.Attributes; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; +namespace Kavita.Server.Controllers; -#nullable enable - -public class ThemeController : BaseApiController +public class ThemeController( + IUnitOfWork unitOfWork, + IThemeService themeService, + ILocalizationService localizationService, + IDirectoryService directoryService, + IMapper mapper) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly IThemeService _themeService; - private readonly ILocalizationService _localizationService; - private readonly IDirectoryService _directoryService; - private readonly IMapper _mapper; - - - public ThemeController(IUnitOfWork unitOfWork, IThemeService themeService, - ILocalizationService localizationService, IDirectoryService directoryService, IMapper mapper) - { - _unitOfWork = unitOfWork; - _themeService = themeService; - _localizationService = localizationService; - _directoryService = directoryService; - _mapper = mapper; - } - [HttpGet] public async Task>> GetThemes() { - return Ok(await _unitOfWork.SiteThemeRepository.GetThemeDtos()); + return Ok(await unitOfWork.SiteThemeRepository.GetThemeDtos()); } @@ -50,11 +37,11 @@ public class ThemeController : BaseApiController { try { - await _themeService.UpdateDefault(dto.ThemeId); + await themeService.UpdateDefault(dto.ThemeId); } catch (KavitaException) { - return BadRequest(await _localizationService.Translate(UserId, "theme-doesnt-exist")); + return BadRequest(await localizationService.Translate(UserId, "theme-doesnt-exist")); } return Ok(); @@ -70,11 +57,11 @@ public class ThemeController : BaseApiController { try { - return Ok(await _themeService.GetContent(themeId)); + return Ok(await themeService.GetContent(themeId)); } catch (KavitaException ex) { - return BadRequest(await _localizationService.Get("en", ex.Message)); + return BadRequest(await localizationService.Get("en", ex.Message)); } } @@ -86,7 +73,7 @@ public class ThemeController : BaseApiController [ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute)] public async Task>> BrowseThemes() { - var themes = await _themeService.GetDownloadableThemes(); + var themes = await themeService.GetDownloadableThemes(); return Ok(themes.Where(t => !t.AlreadyDownloaded)); } @@ -99,7 +86,7 @@ public class ThemeController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task>> DeleteTheme(int themeId) { - await _themeService.DeleteTheme(themeId); + await themeService.DeleteTheme(themeId); return Ok(); } @@ -112,7 +99,7 @@ public class ThemeController : BaseApiController [HttpPost("download-theme")] public async Task> DownloadTheme(DownloadableSiteThemeDto dto) { - return Ok(_mapper.Map(await _themeService.DownloadRepoTheme(dto))); + return Ok(mapper.Map(await themeService.DownloadRepoTheme(dto))); } /// @@ -129,13 +116,13 @@ public class ThemeController : BaseApiController var tempFile = await UploadToTemp(formFile); // Set summary as "Uploaded by Username! on DATE" - var theme = await _themeService.CreateThemeFromFile(tempFile, Username!); - return Ok(_mapper.Map(theme)); + var theme = await themeService.CreateThemeFromFile(tempFile, Username!); + return Ok(mapper.Map(theme)); } private async Task UploadToTemp(IFormFile file) { - var outputFile = Path.Join(_directoryService.TempDirectory, file.FileName); + var outputFile = Path.Join(directoryService.TempDirectory, file.FileName); await using var stream = System.IO.File.Create(outputFile); await file.CopyToAsync(stream); stream.Close(); diff --git a/API/Controllers/UploadController.cs b/Kavita.Server/Controllers/UploadController.cs similarity index 96% rename from API/Controllers/UploadController.cs rename to Kavita.Server/Controllers/UploadController.cs index 7aabc5d8c..ea24e4502 100644 --- a/API/Controllers/UploadController.cs +++ b/Kavita.Server/Controllers/UploadController.cs @@ -1,24 +1,26 @@ using System; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Uploads; -using API.Entities.Enums; -using API.Entities.MetadataMatching; -using API.Extensions; -using API.Middleware; -using API.Services; -using API.Services.Tasks.Metadata; -using API.SignalR; using Flurl.Http; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Metadata; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; +using Kavita.Common.Extensions; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.DTOs.Uploads; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.MetadataMatching; +using Kavita.Server.Attributes; +using Kavita.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace API.Controllers; - -#nullable enable +namespace Kavita.Server.Controllers; [SkipDeviceTracking] public class UploadController : BaseApiController @@ -151,9 +153,8 @@ public class UploadController : BaseApiController /// /// /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] - [RequestSizeLimit(ControllerConstants.MaxUploadSizeBytes)] [HttpPost("collection")] + [RequestSizeLimit(ControllerConstants.MaxUploadSizeBytes)] public async Task UploadCollectionCoverImageFromUrl(UploadFileDto uploadFileDto) { // Check if Url is non empty, request the image and place in temp, then ask image service to handle it. @@ -163,6 +164,9 @@ public class UploadController : BaseApiController var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(uploadFileDto.Id); if (tag == null) return BadRequest(await _localizationService.Translate(UserId, "collection-doesnt-exist")); + if (!User.IsInRole(PolicyConstants.AdminRole) && tag.AppUserId != UserId) + return Unauthorized(); + var filePath = string.Empty; var lockState = false; if (!string.IsNullOrEmpty(uploadFileDto.Url)) @@ -486,7 +490,7 @@ public class UploadController : BaseApiController { try { - if (uploadFileDto.Id != UserId) return Forbid(); + if (uploadFileDto.Id != UserId) return NotFound(); var user = await _unitOfWork.UserRepository.GetUserByIdAsync(uploadFileDto.Id); if (user == null) return BadRequest(await _localizationService.Translate(UserId, "user-doesnt-exist")); diff --git a/API/Controllers/UsersController.cs b/Kavita.Server/Controllers/UsersController.cs similarity index 61% rename from API/Controllers/UsersController.cs rename to Kavita.Server/Controllers/UsersController.cs index 48d2a8e47..2142327e1 100644 --- a/API/Controllers/UsersController.cs +++ b/Kavita.Server/Controllers/UsersController.cs @@ -2,63 +2,55 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Account; -using API.DTOs.KavitaPlus.Account; -using API.Middleware; -using API.Services; -using API.Services.Plus; -using API.SignalR; using AutoMapper; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Services.SignalR; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Account; +using Kavita.Models.DTOs.KavitaPlus.Account; +using Kavita.Models.DTOs.SignalR; +using Kavita.Server.Attributes; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; +namespace Kavita.Server.Controllers; #nullable enable [Authorize] -public class UsersController : BaseApiController +public class UsersController( + IUnitOfWork unitOfWork, + IMapper mapper, + IEventHub eventHub, + ILocalizationService localizationService, + ILicenseService licenseService) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly IMapper _mapper; - private readonly IEventHub _eventHub; - private readonly ILocalizationService _localizationService; - private readonly ILicenseService _licenseService; - - public UsersController(IUnitOfWork unitOfWork, IMapper mapper, IEventHub eventHub, - ILocalizationService localizationService, ILicenseService licenseService) - { - _unitOfWork = unitOfWork; - _mapper = mapper; - _eventHub = eventHub; - _localizationService = localizationService; - _licenseService = licenseService; - } - [Authorize(Policy = PolicyGroups.AdminPolicy)] [HttpDelete("delete-user")] public async Task DeleteUser(string username) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(username); + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(username); if (user == null) return BadRequest(); // Remove all likes for the user, so like counts are correct - var annotations = await _unitOfWork.AnnotationRepository.GetAllAnnotations(); + var annotations = await unitOfWork.AnnotationRepository.GetAllAnnotations(); foreach (var annotation in annotations.Where(a => a.Likes.Contains(user.Id))) { annotation.Likes.Remove(user.Id); - _unitOfWork.AnnotationRepository.Update(annotation); + unitOfWork.AnnotationRepository.Update(annotation); } - _unitOfWork.UserRepository.Delete(user); + unitOfWork.UserRepository.Delete(user); - if (await _unitOfWork.CommitAsync()) return Ok(); + if (await unitOfWork.CommitAsync()) return Ok(); - return BadRequest(await _localizationService.Translate(UserId, "generic-user-delete")); + return BadRequest(await localizationService.Translate(UserId, "generic-user-delete")); } /// @@ -70,7 +62,7 @@ public class UsersController : BaseApiController [HttpGet] public async Task>> GetUsers(bool includePending = false) { - return Ok(await _unitOfWork.UserRepository.GetEmailConfirmedMemberDtosAsync(!includePending)); + return Ok(await unitOfWork.UserRepository.GetEmailConfirmedMemberDtosAsync(!includePending)); } /// @@ -83,10 +75,10 @@ public class UsersController : BaseApiController public async Task> GetProfileInfo(int userId) { // Validate that the user has sharing enabled - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId); if (user == null) return BadRequest(); - return Ok(_mapper.Map(user)); + return Ok(mapper.Map(user)); } /// @@ -98,7 +90,7 @@ public class UsersController : BaseApiController [Authorize] public async Task> HasProfileShared(int userId) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); return Ok(user?.UserPreferences?.SocialPreferences?.ShareProfile ?? false); } @@ -110,9 +102,9 @@ public class UsersController : BaseApiController [HttpGet("has-reading-progress")] public async Task> HasReadingProgress(int libraryId) { - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); - if (library == null) return BadRequest(await _localizationService.Translate(UserId, "library-doesnt-exist")); - return Ok(await _unitOfWork.AppUserProgressRepository.UserHasProgress(library.Type, UserId)); + var library = await unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); + if (library == null) return BadRequest(await localizationService.Translate(UserId, "library-doesnt-exist")); + return Ok(await unitOfWork.AppUserProgressRepository.UserHasProgress(library.Type, UserId)); } /// @@ -123,7 +115,7 @@ public class UsersController : BaseApiController [HttpGet("has-library-access")] public async Task< ActionResult> HasLibraryAccess(int libraryId) { - var libs = await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(Username!); + var libs = await unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(Username!); return Ok(libs.Any(x => x.Id == libraryId)); } @@ -137,7 +129,7 @@ public class UsersController : BaseApiController [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> UpdatePreferences(UserPreferencesDto preferencesDto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.UserPreferences); if (user == null) return Unauthorized(); @@ -154,7 +146,7 @@ public class UsersController : BaseApiController existingPreferences.PromptForRereadsAfter = Math.Max(preferencesDto.PromptForRereadsAfter, 0); existingPreferences.CustomKeyBinds = preferencesDto.CustomKeyBinds; - var allLibs = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)) + var allLibs = (await unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)) .Select(l => l.Id).ToList(); preferencesDto.SocialPreferences.SocialLibraries = preferencesDto.SocialPreferences.SocialLibraries @@ -163,7 +155,7 @@ public class UsersController : BaseApiController existingPreferences.OpdsPreferences = preferencesDto.OpdsPreferences; - if (await _licenseService.HasActiveLicense()) + if (await licenseService.HasActiveLicense(ct: HttpContext.RequestAborted)) { existingPreferences.AniListScrobblingEnabled = preferencesDto.AniListScrobblingEnabled; existingPreferences.WantToReadSync = preferencesDto.WantToReadSync; @@ -173,22 +165,22 @@ public class UsersController : BaseApiController if (preferencesDto.Theme != null && existingPreferences.Theme.Id != preferencesDto.Theme?.Id) { - var theme = await _unitOfWork.SiteThemeRepository.GetTheme(preferencesDto.Theme!.Id); - existingPreferences.Theme = theme ?? await _unitOfWork.SiteThemeRepository.GetDefaultTheme(); + var theme = await unitOfWork.SiteThemeRepository.GetTheme(preferencesDto.Theme!.Id); + existingPreferences.Theme = theme ?? await unitOfWork.SiteThemeRepository.GetDefaultTheme(); } - if (_localizationService.GetLocales().Select(l => l.FileName).Contains(preferencesDto.Locale)) + if (localizationService.GetLocales().Select(l => l.FileName).Contains(preferencesDto.Locale)) { existingPreferences.Locale = preferencesDto.Locale; } - _unitOfWork.UserRepository.Update(existingPreferences); + unitOfWork.UserRepository.Update(existingPreferences); - if (!await _unitOfWork.CommitAsync()) return BadRequest(await _localizationService.Translate(UserId, "generic-user-pref")); + if (!await unitOfWork.CommitAsync()) return BadRequest(await localizationService.Translate(UserId, "generic-user-pref")); - await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id); + await eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id); return Ok(preferencesDto); } @@ -199,8 +191,8 @@ public class UsersController : BaseApiController [HttpGet("get-preferences")] public async Task> GetPreferences() { - return _mapper.Map( - await _unitOfWork.UserRepository.GetPreferencesAsync(Username!)); + return mapper.Map( + await unitOfWork.UserRepository.GetPreferencesAsync(Username!)); } @@ -212,7 +204,7 @@ public class UsersController : BaseApiController [HttpGet("names")] public async Task>> GetUserNames() { - return Ok((await _unitOfWork.UserRepository.GetAllUsersAsync()).Select(u => u.UserName)); + return Ok((await unitOfWork.UserRepository.GetAllUsersAsync()).Select(u => u.UserName)); } /// @@ -220,12 +212,11 @@ public class UsersController : BaseApiController /// /// Kavita+ only /// - [Authorize(Policy = PolicyGroups.AdminPolicy)] + [KPlus] [HttpGet("tokens")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task>> GetUserTokens() { - if (!await _licenseService.HasActiveLicense()) return BadRequest(_localizationService.Translate(UserId, "kavitaplus-restricted")); - - return Ok((await _unitOfWork.UserRepository.GetUserTokenInfo())); + return Ok(await unitOfWork.UserRepository.GetUserTokenInfo()); } } diff --git a/Kavita.Server/Controllers/VolumeController.cs b/Kavita.Server/Controllers/VolumeController.cs new file mode 100644 index 000000000..7b2a76804 --- /dev/null +++ b/Kavita.Server/Controllers/VolumeController.cs @@ -0,0 +1,74 @@ +using System.Threading.Tasks; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; +using Kavita.Models.Constants; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.SignalR; +using Kavita.Server.Attributes; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Kavita.Server.Controllers; + +public class VolumeController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IEventHub eventHub) + : BaseApiController +{ + /// + /// Returns the appropriate Volume + /// + /// + /// + [VolumeAccess] + [HttpGet] + public async Task> GetVolume(int volumeId) + { + return Ok(await unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, UserId)); + } + + [HttpDelete] + [Authorize(Policy = PolicyGroups.AdminPolicy)] + public async Task> DeleteVolume(int volumeId) + { + var volume = await unitOfWork.VolumeRepository.GetVolumeByIdAsync(volumeId, + VolumeIncludes.Chapters | VolumeIncludes.People | VolumeIncludes.Tags); + if (volume == null) + return BadRequest(localizationService.Translate(UserId, "volume-doesnt-exist")); + + unitOfWork.VolumeRepository.Remove(volume); + + if (await unitOfWork.CommitAsync()) + { + await eventHub.SendMessageAsync(MessageFactory.VolumeRemoved, MessageFactory.VolumeRemovedEvent(volume.Id, volume.SeriesId), false); + return Ok(true); + } + + return Ok(false); + } + + [HttpPost("multiple")] + [Authorize(Policy = PolicyGroups.AdminPolicy)] + public async Task> DeleteMultipleVolumes(int[] volumesIds) + { + var volumes = await unitOfWork.VolumeRepository.GetVolumesById(volumesIds); + if (volumes.Count != volumesIds.Length) + { + return BadRequest(localizationService.Translate(UserId, "volume-doesnt-exist")); + } + + unitOfWork.VolumeRepository.Remove(volumes); + + if (!await unitOfWork.CommitAsync()) + { + return Ok(false); + } + + foreach (var volume in volumes) + { + await eventHub.SendMessageAsync(MessageFactory.VolumeRemoved, MessageFactory.VolumeRemovedEvent(volume.Id, volume.SeriesId), false); + } + + return Ok(true); + } +} diff --git a/API/Controllers/WantToReadController.cs b/Kavita.Server/Controllers/WantToReadController.cs similarity index 56% rename from API/Controllers/WantToReadController.cs rename to Kavita.Server/Controllers/WantToReadController.cs index 33d8b0b15..e87a4d68d 100644 --- a/API/Controllers/WantToReadController.cs +++ b/Kavita.Server/Controllers/WantToReadController.cs @@ -1,45 +1,32 @@ -using System; -using System.Linq; +using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -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.Middleware; -using API.Services; -using API.Services.Plus; using Hangfire; +using Kavita.API.Attributes; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.DTOs.WantToRead; +using Kavita.Models.Entities.User; +using Kavita.Server.Attributes; +using Kavita.Server.Extensions; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers; - -#nullable enable - +namespace Kavita.Server.Controllers; /// /// Responsible for all things Want To Read /// [Route("api/want-to-read")] -public class WantToReadController : BaseApiController +public class WantToReadController( + IUnitOfWork unitOfWork, + IScrobblingService scrobblingService, + ILocalizationService localizationService, + ISeriesService seriesService) + : BaseApiController { - private readonly IUnitOfWork _unitOfWork; - private readonly IScrobblingService _scrobblingService; - private readonly ILocalizationService _localizationService; - private readonly ISeriesService _seriesService; - - public WantToReadController(IUnitOfWork unitOfWork, IScrobblingService scrobblingService, - ILocalizationService localizationService, ISeriesService seriesService) - { - _unitOfWork = unitOfWork; - _scrobblingService = scrobblingService; - _localizationService = localizationService; - _seriesService = seriesService; - } - /// /// Return all Series that are in the current logged in user's Want to Read list, filtered /// @@ -55,18 +42,19 @@ public class WantToReadController : BaseApiController userParams ??= new UserParams(); // Add profile privacy filter - filterDto.Statements.AddRange(await _seriesService.GetProfilePrivacyStatements(wantToReadForUser, UserId)); + filterDto.Statements.AddRange(await seriesService.GetProfilePrivacyStatements(wantToReadForUser, UserId)); - var pagedList = await _unitOfWork.SeriesRepository.GetWantToReadForUserV2Async(wantToReadForUser, userParams, filterDto); + var pagedList = await unitOfWork.SeriesRepository.GetWantToReadForUserV2Async(wantToReadForUser, userParams, filterDto); Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages); return Ok(pagedList); } [HttpGet] + [SeriesAccess] public async Task> IsSeriesInWantToRead([FromQuery] int seriesId) { - return Ok(await _unitOfWork.SeriesRepository.IsSeriesInWantToRead(UserId, seriesId)); + return Ok(await unitOfWork.SeriesRepository.IsSeriesInWantToRead(UserId, seriesId)); } /// @@ -77,7 +65,7 @@ public class WantToReadController : BaseApiController [HttpPost("add-series")] public async Task AddSeries(UpdateWantToReadDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.WantToRead); if (user == null) return Unauthorized(); @@ -92,17 +80,17 @@ public class WantToReadController : BaseApiController }); } - if (!_unitOfWork.HasChanges()) return Ok(); - if (await _unitOfWork.CommitAsync()) + if (!unitOfWork.HasChanges()) return Ok(); + if (await unitOfWork.CommitAsync()) { foreach (var sId in dto.SeriesIds) { - BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleWantToReadUpdate(user.Id, sId, true)); + BackgroundJob.Enqueue(() => scrobblingService.ScrobbleWantToReadUpdate(user.Id, sId, true)); } return Ok(); } - return BadRequest(await _localizationService.Translate(UserId, "generic-reading-list-update")); + return BadRequest(await localizationService.Translate(UserId, "generic-reading-list-update")); } /// @@ -113,7 +101,7 @@ public class WantToReadController : BaseApiController [HttpPost("remove-series")] public async Task RemoveSeries(UpdateWantToReadDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(Username!, AppUserIncludes.WantToRead); if (user == null) return Unauthorized(); @@ -121,17 +109,17 @@ public class WantToReadController : BaseApiController .Where(s => !dto.SeriesIds.Contains(s.SeriesId)) .ToList(); - if (!_unitOfWork.HasChanges()) return Ok(); - if (await _unitOfWork.CommitAsync()) + if (!unitOfWork.HasChanges()) return Ok(); + if (await unitOfWork.CommitAsync()) { foreach (var sId in dto.SeriesIds) { - BackgroundJob.Enqueue(() => _scrobblingService.ScrobbleWantToReadUpdate(user.Id, sId, false)); + BackgroundJob.Enqueue(() => scrobblingService.ScrobbleWantToReadUpdate(user.Id, sId, false)); } return Ok(); } - return BadRequest(await _localizationService.Translate(UserId, "generic-reading-list-update")); + return BadRequest(await localizationService.Translate(UserId, "generic-reading-list-update")); } } diff --git a/API/EmailTemplates/AuthKeyExpired.html b/Kavita.Server/EmailTemplates/AuthKeyExpired.html similarity index 100% rename from API/EmailTemplates/AuthKeyExpired.html rename to Kavita.Server/EmailTemplates/AuthKeyExpired.html diff --git a/API/EmailTemplates/AuthKeyExpiredFragment.html b/Kavita.Server/EmailTemplates/AuthKeyExpiredFragment.html similarity index 100% rename from API/EmailTemplates/AuthKeyExpiredFragment.html rename to Kavita.Server/EmailTemplates/AuthKeyExpiredFragment.html diff --git a/API/EmailTemplates/AuthKeyExpiringFragment.html b/Kavita.Server/EmailTemplates/AuthKeyExpiringFragment.html similarity index 100% rename from API/EmailTemplates/AuthKeyExpiringFragment.html rename to Kavita.Server/EmailTemplates/AuthKeyExpiringFragment.html diff --git a/API/EmailTemplates/AuthKeyExpiringSoon.html b/Kavita.Server/EmailTemplates/AuthKeyExpiringSoon.html similarity index 100% rename from API/EmailTemplates/AuthKeyExpiringSoon.html rename to Kavita.Server/EmailTemplates/AuthKeyExpiringSoon.html diff --git a/API/EmailTemplates/EmailChange.html b/Kavita.Server/EmailTemplates/EmailChange.html similarity index 100% rename from API/EmailTemplates/EmailChange.html rename to Kavita.Server/EmailTemplates/EmailChange.html diff --git a/API/EmailTemplates/EmailConfirm.html b/Kavita.Server/EmailTemplates/EmailConfirm.html similarity index 100% rename from API/EmailTemplates/EmailConfirm.html rename to Kavita.Server/EmailTemplates/EmailConfirm.html diff --git a/API/EmailTemplates/EmailPasswordReset.html b/Kavita.Server/EmailTemplates/EmailPasswordReset.html similarity index 100% rename from API/EmailTemplates/EmailPasswordReset.html rename to Kavita.Server/EmailTemplates/EmailPasswordReset.html diff --git a/API/EmailTemplates/EmailTest.html b/Kavita.Server/EmailTemplates/EmailTest.html similarity index 100% rename from API/EmailTemplates/EmailTest.html rename to Kavita.Server/EmailTemplates/EmailTest.html diff --git a/API/EmailTemplates/KavitaPlusDebug.html b/Kavita.Server/EmailTemplates/KavitaPlusDebug.html similarity index 100% rename from API/EmailTemplates/KavitaPlusDebug.html rename to Kavita.Server/EmailTemplates/KavitaPlusDebug.html diff --git a/API/EmailTemplates/SendToDevice.html b/Kavita.Server/EmailTemplates/SendToDevice.html similarity index 100% rename from API/EmailTemplates/SendToDevice.html rename to Kavita.Server/EmailTemplates/SendToDevice.html diff --git a/API/EmailTemplates/TokenExpiration.html b/Kavita.Server/EmailTemplates/TokenExpiration.html similarity index 100% rename from API/EmailTemplates/TokenExpiration.html rename to Kavita.Server/EmailTemplates/TokenExpiration.html diff --git a/API/EmailTemplates/TokenExpiringSoon.html b/Kavita.Server/EmailTemplates/TokenExpiringSoon.html similarity index 100% rename from API/EmailTemplates/TokenExpiringSoon.html rename to Kavita.Server/EmailTemplates/TokenExpiringSoon.html diff --git a/API/EmailTemplates/base.html b/Kavita.Server/EmailTemplates/base.html similarity index 100% rename from API/EmailTemplates/base.html rename to Kavita.Server/EmailTemplates/base.html diff --git a/Kavita.Server/Extensions/ApplicationServiceExtensions.cs b/Kavita.Server/Extensions/ApplicationServiceExtensions.cs new file mode 100644 index 000000000..52309edec --- /dev/null +++ b/Kavita.Server/Extensions/ApplicationServiceExtensions.cs @@ -0,0 +1,62 @@ +using Kavita.API.Services; +using Kavita.API.Store; +using Kavita.Common; +using Kavita.Database.Extensions; +using Kavita.Models.Constants; +using Kavita.Server.Logging; +using Kavita.Server.Middleware; +using Kavita.Server.Store; +using Kavita.Services.Extensions; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Kavita.Server.Extensions; + + +public static class ApplicationServiceExtensions +{ + public static void AddApplicationServices(this IServiceCollection services, IConfiguration config, IWebHostEnvironment env) + { + services.AddScoped(); + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(sp => sp.GetRequiredService()); + + services.AddKavitaDatabases(); + services.AddKavitaServices(); + + services.AddSignalR(opt => opt.EnableDetailedErrors = true); + + services.AddEasyCaching(options => + { + options.UseInMemory(EasyCacheProfiles.Favicon); + options.UseInMemory(EasyCacheProfiles.Publisher); + options.UseInMemory(EasyCacheProfiles.Library); + options.UseInMemory(EasyCacheProfiles.RevokedJwt); + options.UseInMemory(EasyCacheProfiles.LocaleOptions); + + // KavitaPlus stuff + options.UseInMemory(EasyCacheProfiles.KavitaPlusExternalSeries); + options.UseInMemory(EasyCacheProfiles.License); + options.UseInMemory(EasyCacheProfiles.LicenseInfo); + options.UseInMemory(EasyCacheProfiles.KavitaPlusMatchSeries); + }); + + services.AddMemoryCache(options => + { + options.SizeLimit = Configuration.CacheSize * 1024 * 1024; // 75 MB + options.CompactionPercentage = 0.1; // LRU compaction, Evict 10% when limit reached + }); + + services.AddSingleton(); + services.AddSingleton(); + + services.AddSwaggerGen(g => + { + g.UseInlineDefinitionsForEnums(); + }); + } +} diff --git a/API/Extensions/HttpExtensions.cs b/Kavita.Server/Extensions/HttpExtensions.cs similarity index 93% rename from API/Extensions/HttpExtensions.cs rename to Kavita.Server/Extensions/HttpExtensions.cs index de8f59c36..fa30b9968 100644 --- a/API/Extensions/HttpExtensions.cs +++ b/Kavita.Server/Extensions/HttpExtensions.cs @@ -1,9 +1,8 @@ -using System.Text.Json; -using API.Helpers; +using System.Text.Json; +using Kavita.Common.Helpers; using Microsoft.AspNetCore.Http; -namespace API.Extensions; -#nullable enable +namespace Kavita.Server.Extensions; public static class HttpExtensions { diff --git a/API/Extensions/IdentityServiceExtensions.cs b/Kavita.Server/Extensions/IdentityServiceExtensions.cs similarity index 96% rename from API/Extensions/IdentityServiceExtensions.cs rename to Kavita.Server/Extensions/IdentityServiceExtensions.cs index 3da1aed22..ce2bfc31c 100644 --- a/API/Extensions/IdentityServiceExtensions.cs +++ b/Kavita.Server/Extensions/IdentityServiceExtensions.cs @@ -5,14 +5,15 @@ using System.Linq; using System.Security.Claims; using System.Text; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Entities; -using API.Entities.Progress; -using API.Helpers; -using API.Middleware; -using API.Services; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Database; +using Kavita.Models.Constants; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; +using Kavita.Server.Helpers; +using Kavita.Server.Middleware; +using Kavita.Services; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.JwtBearer; @@ -28,8 +29,7 @@ using Microsoft.IdentityModel.Tokens; using Serilog; using MessageReceivedContext = Microsoft.AspNetCore.Authentication.JwtBearer.MessageReceivedContext; -namespace API.Extensions; -#nullable enable +namespace Kavita.Server.Extensions; public static class IdentityServiceExtensions { @@ -154,7 +154,9 @@ public static class IdentityServiceExtensions .AddPolicy(PolicyGroups.DownloadPolicy, policy => policy.RequireRole(PolicyConstants.DownloadRole, PolicyConstants.AdminRole)) .AddPolicy(PolicyGroups.ChangePasswordPolicy, - policy => policy.RequireRole(PolicyConstants.ChangePasswordRole, PolicyConstants.AdminRole)); + policy => policy.RequireRole(PolicyConstants.ChangePasswordRole, PolicyConstants.AdminRole)) + .AddPolicy(PolicyGroups.BookmarkPolicy, + policy => policy.RequireRole(PolicyConstants.BookmarkRole, PolicyConstants.AdminRole)); return services; } diff --git a/API/Helpers/BrowserHelper.cs b/Kavita.Server/Helpers/BrowserHelper.cs similarity index 97% rename from API/Helpers/BrowserHelper.cs rename to Kavita.Server/Helpers/BrowserHelper.cs index d11e2122f..c7bde8099 100644 --- a/API/Helpers/BrowserHelper.cs +++ b/Kavita.Server/Helpers/BrowserHelper.cs @@ -1,7 +1,6 @@ -using System; -using API.Entities.Enums; +using Kavita.Models.Entities.Enums; -namespace API.Helpers; +namespace Kavita.Server.Helpers; /// /// Handles all things around Parsing Headers diff --git a/API/Helpers/OpenIdConnectEventsHelper.cs b/Kavita.Server/Helpers/OpenIdConnectEventsHelper.cs similarity index 98% rename from API/Helpers/OpenIdConnectEventsHelper.cs rename to Kavita.Server/Helpers/OpenIdConnectEventsHelper.cs index 3605251a2..1ef4143d1 100644 --- a/API/Helpers/OpenIdConnectEventsHelper.cs +++ b/Kavita.Server/Helpers/OpenIdConnectEventsHelper.cs @@ -2,17 +2,17 @@ using System; using System.Security.Claims; using System.Text.Json; using System.Threading.Tasks; -using API.Extensions; -using API.Services; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Server.Extensions; +using Kavita.Services; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Serilog; -namespace API.Helpers; -#nullable enable +namespace Kavita.Server.Helpers; public class OpenIdConnectEventsHelper: OpenIdConnectEvents { diff --git a/API/I18N/ar.json b/Kavita.Server/I18N/ar.json similarity index 100% rename from API/I18N/ar.json rename to Kavita.Server/I18N/ar.json diff --git a/API/I18N/as.json b/Kavita.Server/I18N/as.json similarity index 100% rename from API/I18N/as.json rename to Kavita.Server/I18N/as.json diff --git a/API/I18N/ca.json b/Kavita.Server/I18N/ca.json similarity index 100% rename from API/I18N/ca.json rename to Kavita.Server/I18N/ca.json diff --git a/API/I18N/cs.json b/Kavita.Server/I18N/cs.json similarity index 100% rename from API/I18N/cs.json rename to Kavita.Server/I18N/cs.json diff --git a/API/I18N/da.json b/Kavita.Server/I18N/da.json similarity index 100% rename from API/I18N/da.json rename to Kavita.Server/I18N/da.json diff --git a/API/I18N/de.json b/Kavita.Server/I18N/de.json similarity index 100% rename from API/I18N/de.json rename to Kavita.Server/I18N/de.json diff --git a/API/I18N/el.json b/Kavita.Server/I18N/el.json similarity index 100% rename from API/I18N/el.json rename to Kavita.Server/I18N/el.json diff --git a/API/I18N/en.json b/Kavita.Server/I18N/en.json similarity index 100% rename from API/I18N/en.json rename to Kavita.Server/I18N/en.json diff --git a/API/I18N/es.json b/Kavita.Server/I18N/es.json similarity index 100% rename from API/I18N/es.json rename to Kavita.Server/I18N/es.json diff --git a/API/I18N/et.json b/Kavita.Server/I18N/et.json similarity index 100% rename from API/I18N/et.json rename to Kavita.Server/I18N/et.json diff --git a/API/I18N/fa.json b/Kavita.Server/I18N/fa.json similarity index 100% rename from API/I18N/fa.json rename to Kavita.Server/I18N/fa.json diff --git a/API/I18N/fi.json b/Kavita.Server/I18N/fi.json similarity index 100% rename from API/I18N/fi.json rename to Kavita.Server/I18N/fi.json diff --git a/API/I18N/fr.json b/Kavita.Server/I18N/fr.json similarity index 100% rename from API/I18N/fr.json rename to Kavita.Server/I18N/fr.json diff --git a/API/I18N/ga.json b/Kavita.Server/I18N/ga.json similarity index 100% rename from API/I18N/ga.json rename to Kavita.Server/I18N/ga.json diff --git a/API/I18N/he.json b/Kavita.Server/I18N/he.json similarity index 100% rename from API/I18N/he.json rename to Kavita.Server/I18N/he.json diff --git a/API/I18N/hi.json b/Kavita.Server/I18N/hi.json similarity index 100% rename from API/I18N/hi.json rename to Kavita.Server/I18N/hi.json diff --git a/API/I18N/hr.json b/Kavita.Server/I18N/hr.json similarity index 100% rename from API/I18N/hr.json rename to Kavita.Server/I18N/hr.json diff --git a/API/I18N/hu.json b/Kavita.Server/I18N/hu.json similarity index 100% rename from API/I18N/hu.json rename to Kavita.Server/I18N/hu.json diff --git a/API/I18N/id.json b/Kavita.Server/I18N/id.json similarity index 100% rename from API/I18N/id.json rename to Kavita.Server/I18N/id.json diff --git a/API/I18N/it.json b/Kavita.Server/I18N/it.json similarity index 100% rename from API/I18N/it.json rename to Kavita.Server/I18N/it.json diff --git a/API/I18N/ja.json b/Kavita.Server/I18N/ja.json similarity index 100% rename from API/I18N/ja.json rename to Kavita.Server/I18N/ja.json diff --git a/API/I18N/ko.json b/Kavita.Server/I18N/ko.json similarity index 100% rename from API/I18N/ko.json rename to Kavita.Server/I18N/ko.json diff --git a/API/I18N/lt.json b/Kavita.Server/I18N/lt.json similarity index 100% rename from API/I18N/lt.json rename to Kavita.Server/I18N/lt.json diff --git a/API/I18N/ms.json b/Kavita.Server/I18N/ms.json similarity index 100% rename from API/I18N/ms.json rename to Kavita.Server/I18N/ms.json diff --git a/API/I18N/nb_NO.json b/Kavita.Server/I18N/nb_NO.json similarity index 100% rename from API/I18N/nb_NO.json rename to Kavita.Server/I18N/nb_NO.json diff --git a/API/I18N/nl.json b/Kavita.Server/I18N/nl.json similarity index 100% rename from API/I18N/nl.json rename to Kavita.Server/I18N/nl.json diff --git a/API/I18N/pl.json b/Kavita.Server/I18N/pl.json similarity index 100% rename from API/I18N/pl.json rename to Kavita.Server/I18N/pl.json diff --git a/API/I18N/pt.json b/Kavita.Server/I18N/pt.json similarity index 100% rename from API/I18N/pt.json rename to Kavita.Server/I18N/pt.json diff --git a/API/I18N/pt_BR.json b/Kavita.Server/I18N/pt_BR.json similarity index 100% rename from API/I18N/pt_BR.json rename to Kavita.Server/I18N/pt_BR.json diff --git a/API/I18N/ru.json b/Kavita.Server/I18N/ru.json similarity index 100% rename from API/I18N/ru.json rename to Kavita.Server/I18N/ru.json diff --git a/API/I18N/sk.json b/Kavita.Server/I18N/sk.json similarity index 100% rename from API/I18N/sk.json rename to Kavita.Server/I18N/sk.json diff --git a/API/I18N/sl.json b/Kavita.Server/I18N/sl.json similarity index 100% rename from API/I18N/sl.json rename to Kavita.Server/I18N/sl.json diff --git a/API/I18N/sv.json b/Kavita.Server/I18N/sv.json similarity index 100% rename from API/I18N/sv.json rename to Kavita.Server/I18N/sv.json diff --git a/API/I18N/ta.json b/Kavita.Server/I18N/ta.json similarity index 100% rename from API/I18N/ta.json rename to Kavita.Server/I18N/ta.json diff --git a/API/I18N/te.json b/Kavita.Server/I18N/te.json similarity index 100% rename from API/I18N/te.json rename to Kavita.Server/I18N/te.json diff --git a/API/I18N/th.json b/Kavita.Server/I18N/th.json similarity index 100% rename from API/I18N/th.json rename to Kavita.Server/I18N/th.json diff --git a/API/I18N/tr.json b/Kavita.Server/I18N/tr.json similarity index 100% rename from API/I18N/tr.json rename to Kavita.Server/I18N/tr.json diff --git a/API/I18N/uk.json b/Kavita.Server/I18N/uk.json similarity index 100% rename from API/I18N/uk.json rename to Kavita.Server/I18N/uk.json diff --git a/API/I18N/vi.json b/Kavita.Server/I18N/vi.json similarity index 100% rename from API/I18N/vi.json rename to Kavita.Server/I18N/vi.json diff --git a/API/I18N/zh_Hans.json b/Kavita.Server/I18N/zh_Hans.json similarity index 100% rename from API/I18N/zh_Hans.json rename to Kavita.Server/I18N/zh_Hans.json diff --git a/API/I18N/zh_Hant.json b/Kavita.Server/I18N/zh_Hant.json similarity index 100% rename from API/I18N/zh_Hant.json rename to Kavita.Server/I18N/zh_Hant.json diff --git a/Kavita.Server/Kavita.Server.csproj b/Kavita.Server/Kavita.Server.csproj new file mode 100644 index 000000000..210701930 --- /dev/null +++ b/Kavita.Server/Kavita.Server.csproj @@ -0,0 +1,162 @@ + + + + Exe + Default + net10.0 + true + Linux + true + true + ../favicon.ico + enable + latestmajor + false + disable + + + + false + ../favicon.ico + bin\$(Configuration)\$(AssemblyName).xml + + + + bin\$(Configuration)\$(AssemblyName).xml + 1701;1702;1591 + + + + + True + $(NoWarn);1591 + $(NoWarn);CA1873 + + + + en + + + + + Kavita + kareadita.github.io + Copyright 2020-$([System.DateTime]::Now.ToString('yyyy')) kavitareader.com (GNU General Public v3) + + $(Configuration)-dev + + false + false + false + + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + + + Always + + + + + + + + + Always + + + Always + + + + Always + + + Always + + + + + <_DeploymentManifestIconFile Remove="favicon.ico" /> + + + diff --git a/API/Logging/LogEnricher.cs b/Kavita.Server/Logging/LogEnricher.cs similarity index 95% rename from API/Logging/LogEnricher.cs rename to Kavita.Server/Logging/LogEnricher.cs index e580f663e..b67b5e037 100644 --- a/API/Logging/LogEnricher.cs +++ b/Kavita.Server/Logging/LogEnricher.cs @@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Http; using Serilog; -namespace API.Logging; +namespace Kavita.Server.Logging; public static class LogEnricher { diff --git a/API/Logging/LogLevelOptions.cs b/Kavita.Server/Logging/LogLevelOptions.cs similarity index 97% rename from API/Logging/LogLevelOptions.cs rename to Kavita.Server/Logging/LogLevelOptions.cs index 47df3973d..3d254e042 100644 --- a/API/Logging/LogLevelOptions.cs +++ b/Kavita.Server/Logging/LogLevelOptions.cs @@ -4,7 +4,7 @@ using Serilog.Core; using Serilog.Events; using Serilog.Formatting.Display; -namespace API.Logging; +namespace Kavita.Server.Logging; /// /// This class represents information for configuring Logging in the Application. Only a high log level is exposed and Kavita @@ -49,7 +49,7 @@ public static class LogLevelOptions .MinimumLevel.Override("Microsoft.AspNetCore.ResponseCaching.ResponseCachingMiddleware", LogEventLevel.Error) .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Error) .MinimumLevel.Override("Microsoft.AspNetCore.Authentication", LogEventLevel.Error) - .MinimumLevel.Override("API.Middleware.AuthKeyAuthenticationHandler", LogEventLevel.Error) + .MinimumLevel.Override("Kavita.Server.Middleware.AuthKeyAuthenticationHandler", LogEventLevel.Error) .Enrich.FromLogContext() .Enrich.WithThreadId() .Enrich.With(new ApiKeyEnricher()) diff --git a/Kavita.Server/Logging/LoggingService.cs b/Kavita.Server/Logging/LoggingService.cs new file mode 100644 index 000000000..b2435ac72 --- /dev/null +++ b/Kavita.Server/Logging/LoggingService.cs @@ -0,0 +1,11 @@ +using Kavita.API.Services; + +namespace Kavita.Server.Logging; + +public class LoggingService: ILoggingService +{ + public void SwitchLogLevel(string level) + { + LogLevelOptions.SwitchLogLevel(level); + } +} diff --git a/API/Data/Misc/ManualMigration.cs b/Kavita.Server/ManualMigrations/ManualMigration.cs similarity index 92% rename from API/Data/Misc/ManualMigration.cs rename to Kavita.Server/ManualMigrations/ManualMigration.cs index 907cac648..0625ff67b 100644 --- a/API/Data/Misc/ManualMigration.cs +++ b/Kavita.Server/ManualMigrations/ManualMigration.cs @@ -1,9 +1,10 @@ using System.Threading.Tasks; -using API.Entities.History; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.Misc; +namespace Kavita.Server.ManualMigrations; public abstract class ManualMigration { diff --git a/API/Data/ManualMigrations/v0.7.11/MigrateLibrariesToHaveAllFileTypes.cs b/Kavita.Server/ManualMigrations/v0.7.11/MigrateLibrariesToHaveAllFileTypes.cs similarity index 92% rename from API/Data/ManualMigrations/v0.7.11/MigrateLibrariesToHaveAllFileTypes.cs rename to Kavita.Server/ManualMigrations/v0.7.11/MigrateLibrariesToHaveAllFileTypes.cs index f3197f44b..9c5c9e27b 100644 --- a/API/Data/ManualMigrations/v0.7.11/MigrateLibrariesToHaveAllFileTypes.cs +++ b/Kavita.Server/ManualMigrations/v0.7.11/MigrateLibrariesToHaveAllFileTypes.cs @@ -1,15 +1,12 @@ -using System; -using System.Linq; +using System.Linq; using System.Threading.Tasks; -using API.Data.Misc; -using API.Entities; -using API.Entities.Enums; -using API.Entities.History; -using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._7._11; /// /// Introduced in v0.7.11 with the removal of .Kavitaignore files diff --git a/API/Data/ManualMigrations/v0.7.11/MigrateSmartFilterEncoding.cs b/Kavita.Server/ManualMigrations/v0.7.11/MigrateSmartFilterEncoding.cs similarity index 95% rename from API/Data/ManualMigrations/v0.7.11/MigrateSmartFilterEncoding.cs rename to Kavita.Server/ManualMigrations/v0.7.11/MigrateSmartFilterEncoding.cs index daa62ea77..a9374944c 100644 --- a/API/Data/ManualMigrations/v0.7.11/MigrateSmartFilterEncoding.cs +++ b/Kavita.Server/ManualMigrations/v0.7.11/MigrateSmartFilterEncoding.cs @@ -2,14 +2,15 @@ using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; -using API.DTOs.Filtering.v2; -using API.Entities.History; -using API.Helpers; -using Kavita.Common.EnvironmentInfo; +using Kavita.API.Database; +using Kavita.Database; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.Entities.History; +using Kavita.Services.Helpers; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._7._11; /// /// v0.7.10.2 introduced a bad encoding, this will migrate those bad smart filters diff --git a/API/Data/ManualMigrations/v0.7.14/MigrateClearNightlyExternalSeriesRecords.cs b/Kavita.Server/ManualMigrations/v0.7.14/MigrateClearNightlyExternalSeriesRecords.cs similarity index 90% rename from API/Data/ManualMigrations/v0.7.14/MigrateClearNightlyExternalSeriesRecords.cs rename to Kavita.Server/ManualMigrations/v0.7.14/MigrateClearNightlyExternalSeriesRecords.cs index 92195c9d0..c4b639d7e 100644 --- a/API/Data/ManualMigrations/v0.7.14/MigrateClearNightlyExternalSeriesRecords.cs +++ b/Kavita.Server/ManualMigrations/v0.7.14/MigrateClearNightlyExternalSeriesRecords.cs @@ -1,11 +1,10 @@ -using System; -using System.Threading.Tasks; -using API.Entities.History; -using Kavita.Common.EnvironmentInfo; +using System.Threading.Tasks; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._7._14; /// /// For the v0.7.14 release, one of the nightlies had bad data that would cause issues. This drops those records diff --git a/API/Data/ManualMigrations/v0.7.14/MigrateEmailTemplates.cs b/Kavita.Server/ManualMigrations/v0.7.14/MigrateEmailTemplates.cs similarity index 97% rename from API/Data/ManualMigrations/v0.7.14/MigrateEmailTemplates.cs rename to Kavita.Server/ManualMigrations/v0.7.14/MigrateEmailTemplates.cs index ae0f17c16..ae232cb61 100644 --- a/API/Data/ManualMigrations/v0.7.14/MigrateEmailTemplates.cs +++ b/Kavita.Server/ManualMigrations/v0.7.14/MigrateEmailTemplates.cs @@ -1,11 +1,11 @@ using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Services; using Flurl.Http; +using Kavita.API.Services; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._7._14; public static class MigrateEmailTemplates { diff --git a/API/Data/ManualMigrations/v0.7.14/MigrateManualHistory.cs b/Kavita.Server/ManualMigrations/v0.7.14/MigrateManualHistory.cs similarity index 95% rename from API/Data/ManualMigrations/v0.7.14/MigrateManualHistory.cs rename to Kavita.Server/ManualMigrations/v0.7.14/MigrateManualHistory.cs index 781fd3193..98bac2f03 100644 --- a/API/Data/ManualMigrations/v0.7.14/MigrateManualHistory.cs +++ b/Kavita.Server/ManualMigrations/v0.7.14/MigrateManualHistory.cs @@ -1,10 +1,11 @@ using System; using System.Threading.Tasks; -using API.Entities.History; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._7._14; /// /// Introduced in v0.7.14, will store history so that going forward, migrations can just check against the history diff --git a/API/Data/ManualMigrations/v0.7.14/MigrateVolumeLookupName.cs b/Kavita.Server/ManualMigrations/v0.7.14/MigrateVolumeLookupName.cs similarity index 90% rename from API/Data/ManualMigrations/v0.7.14/MigrateVolumeLookupName.cs rename to Kavita.Server/ManualMigrations/v0.7.14/MigrateVolumeLookupName.cs index 539afd972..3058212e7 100644 --- a/API/Data/ManualMigrations/v0.7.14/MigrateVolumeLookupName.cs +++ b/Kavita.Server/ManualMigrations/v0.7.14/MigrateVolumeLookupName.cs @@ -1,12 +1,14 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; +using Kavita.API.Database; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._7._14; public static class MigrateVolumeLookupName { diff --git a/API/Data/ManualMigrations/v0.7.14/MigrateVolumeNumber.cs b/Kavita.Server/ManualMigrations/v0.7.14/MigrateVolumeNumber.cs similarity index 92% rename from API/Data/ManualMigrations/v0.7.14/MigrateVolumeNumber.cs rename to Kavita.Server/ManualMigrations/v0.7.14/MigrateVolumeNumber.cs index 73b2896fc..add1c0d2d 100644 --- a/API/Data/ManualMigrations/v0.7.14/MigrateVolumeNumber.cs +++ b/Kavita.Server/ManualMigrations/v0.7.14/MigrateVolumeNumber.cs @@ -1,10 +1,11 @@ using System.Threading.Tasks; -using API.Entities.History; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Database; +using Kavita.Models.Entities.History; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._7._14; /// /// Introduced in v0.7.14, this migrates the existing Volume Name -> Volume Min/Max Number diff --git a/API/Data/ManualMigrations/v0.7.14/MigrateWantToReadExport.cs b/Kavita.Server/ManualMigrations/v0.7.14/MigrateWantToReadExport.cs similarity index 95% rename from API/Data/ManualMigrations/v0.7.14/MigrateWantToReadExport.cs rename to Kavita.Server/ManualMigrations/v0.7.14/MigrateWantToReadExport.cs index 62d5bb076..3ca677a1f 100644 --- a/API/Data/ManualMigrations/v0.7.14/MigrateWantToReadExport.cs +++ b/Kavita.Server/ManualMigrations/v0.7.14/MigrateWantToReadExport.cs @@ -2,13 +2,13 @@ using System.Globalization; using System.IO; using System.Threading.Tasks; -using API.Data.Misc; -using API.Services; using CsvHelper; +using Kavita.API.Services; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._7._14; /// /// v0.7.13.12/v0.7.14 - Want to read is extracted and saved in a csv diff --git a/API/Data/ManualMigrations/v0.7.14/MigrateWantToReadImport.cs b/Kavita.Server/ManualMigrations/v0.7.14/MigrateWantToReadImport.cs similarity index 92% rename from API/Data/ManualMigrations/v0.7.14/MigrateWantToReadImport.cs rename to Kavita.Server/ManualMigrations/v0.7.14/MigrateWantToReadImport.cs index d336937c9..a028d7162 100644 --- a/API/Data/ManualMigrations/v0.7.14/MigrateWantToReadImport.cs +++ b/Kavita.Server/ManualMigrations/v0.7.14/MigrateWantToReadImport.cs @@ -2,14 +2,15 @@ using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Data.Misc; -using API.Data.Repositories; -using API.Entities; -using API.Services; using CsvHelper; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.Database; +using Kavita.Models.Entities.User; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._7._14; /// /// v0.7.13.12/v0.7.14 - Want to read is imported from a csv diff --git a/API/Data/ManualMigrations/v0.7.9/MigrateUserLibrarySideNavStream.cs b/Kavita.Server/ManualMigrations/v0.7.9/MigrateUserLibrarySideNavStream.cs similarity index 90% rename from API/Data/ManualMigrations/v0.7.9/MigrateUserLibrarySideNavStream.cs rename to Kavita.Server/ManualMigrations/v0.7.9/MigrateUserLibrarySideNavStream.cs index 9fea24199..d42ebd479 100644 --- a/API/Data/ManualMigrations/v0.7.9/MigrateUserLibrarySideNavStream.cs +++ b/Kavita.Server/ManualMigrations/v0.7.9/MigrateUserLibrarySideNavStream.cs @@ -1,14 +1,15 @@ -using System; -using System.Linq; +using System.Linq; using System.Threading.Tasks; -using API.Data.Repositories; -using API.Entities; -using API.Entities.History; -using Kavita.Common.EnvironmentInfo; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.Database; +using Kavita.Models.Entities; +using Kavita.Models.Entities.History; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._7._9; /// /// Introduced in v0.7.8.7 and v0.7.9, this adds SideNavStream's for all Libraries a User has access to diff --git a/API/Data/ManualMigrations/v0.8.0/ManualMigrateLooseLeafChapters.cs b/Kavita.Server/ManualMigrations/v0.8.0/ManualMigrateLooseLeafChapters.cs similarity index 94% rename from API/Data/ManualMigrations/v0.8.0/ManualMigrateLooseLeafChapters.cs rename to Kavita.Server/ManualMigrations/v0.8.0/ManualMigrateLooseLeafChapters.cs index fac184dc9..ba64f1f83 100644 --- a/API/Data/ManualMigrations/v0.8.0/ManualMigrateLooseLeafChapters.cs +++ b/Kavita.Server/ManualMigrations/v0.8.0/ManualMigrateLooseLeafChapters.cs @@ -2,17 +2,21 @@ using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Entities; -using API.Entities.History; -using API.Extensions; -using API.Helpers.Builders; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.Common.Constants; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Constants; +using Kavita.Models.Entities; +using Kavita.Models.Entities.History; +using Kavita.Models.Extensions; +using Kavita.Services; +using Kavita.Services.Builders; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._0; /// @@ -78,7 +82,7 @@ public static class MigrateLooseLeafChapters var chapters = await dataContext.Chapter .Where(c => c.VolumeId == distinctVolume.Volume.Id && !c.IsSpecial).ToListAsync(); - var newVolume = new VolumeBuilder(Parser.LooseLeafVolume) + var newVolume = new VolumeBuilder(ParserConstants.LooseLeafVolume) .WithSeriesId(seriesId) .WithCreated(distinctVolume.Volume.Created) .WithLastModified(distinctVolume.Volume.LastModified) diff --git a/API/Data/ManualMigrations/v0.8.0/ManualMigrateMixedSpecials.cs b/Kavita.Server/ManualMigrations/v0.8.0/ManualMigrateMixedSpecials.cs similarity index 95% rename from API/Data/ManualMigrations/v0.8.0/ManualMigrateMixedSpecials.cs rename to Kavita.Server/ManualMigrations/v0.8.0/ManualMigrateMixedSpecials.cs index cda83f05b..c93f5c8ec 100644 --- a/API/Data/ManualMigrations/v0.8.0/ManualMigrateMixedSpecials.cs +++ b/Kavita.Server/ManualMigrations/v0.8.0/ManualMigrateMixedSpecials.cs @@ -2,17 +2,21 @@ using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Entities; -using API.Entities.History; -using API.Extensions; -using API.Helpers.Builders; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.Common.Constants; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Constants; +using Kavita.Models.Entities; +using Kavita.Models.Entities.History; +using Kavita.Models.Extensions; +using Kavita.Services; +using Kavita.Services.Builders; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._0; public class UserProgressCsvRecord { @@ -95,7 +99,7 @@ public static class MigrateMixedSpecials var chapters = await dataContext.Chapter .Where(c => c.VolumeId == distinctVolume.Volume.Id && c.IsSpecial).ToListAsync(); - var newVolume = new VolumeBuilder(Parser.SpecialVolume) + var newVolume = new VolumeBuilder(ParserConstants.SpecialVolume) .WithSeriesId(seriesId) .WithCreated(distinctVolume.Volume.Created) .WithLastModified(distinctVolume.Volume.LastModified) diff --git a/API/Data/ManualMigrations/v0.8.0/MigrateChapterFields.cs b/Kavita.Server/ManualMigrations/v0.8.0/MigrateChapterFields.cs similarity index 76% rename from API/Data/ManualMigrations/v0.8.0/MigrateChapterFields.cs rename to Kavita.Server/ManualMigrations/v0.8.0/MigrateChapterFields.cs index 4187788ab..b4c35f37a 100644 --- a/API/Data/ManualMigrations/v0.8.0/MigrateChapterFields.cs +++ b/Kavita.Server/ManualMigrations/v0.8.0/MigrateChapterFields.cs @@ -1,13 +1,17 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; -using API.Services.Tasks.Scanner.Parser; +using Kavita.API.Database; +using Kavita.Common.Constants; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Constants; +using Kavita.Models.Entities.History; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._0; @@ -35,9 +39,9 @@ public static class MigrateChapterFields "Running MigrateChapterFields migration - Updating {Count} volumes that only have specials in them", volumesWithJustSpecials.Count); foreach (var volume in volumesWithJustSpecials) { - volume.Name = $"{Parser.SpecialVolumeNumber}"; - volume.MinNumber = Parser.SpecialVolumeNumber; - volume.MaxNumber = Parser.SpecialVolumeNumber; + volume.Name = $"{ParserConstants.SpecialVolumeNumber}"; + volume.MinNumber = ParserConstants.SpecialVolumeNumber; + volume.MaxNumber = ParserConstants.SpecialVolumeNumber; } // Update all volumes that only have loose leafs in them @@ -49,9 +53,9 @@ public static class MigrateChapterFields "Running MigrateChapterFields migration - Updating {Count} volumes that only have loose leaf chapters in them", looseLeafVolumes.Count); foreach (var volume in looseLeafVolumes) { - volume.Name = $"{Parser.DefaultChapterNumber}"; - volume.MinNumber = Parser.DefaultChapterNumber; - volume.MaxNumber = Parser.DefaultChapterNumber; + volume.Name = $"{ParserConstants.DefaultChapterNumber}"; + volume.MinNumber = ParserConstants.DefaultChapterNumber; + volume.MaxNumber = ParserConstants.DefaultChapterNumber; } // Update all MangaFile @@ -67,9 +71,9 @@ public static class MigrateChapterFields "Running MigrateChapterFields migration - Updating {Count} loose leaf chapters", looseLeafChapters.Count); foreach (var chapter in looseLeafChapters) { - chapter.Number = Parser.DefaultChapter; - chapter.MinNumber = Parser.DefaultChapterNumber; - chapter.MaxNumber = Parser.DefaultChapterNumber; + chapter.Number = ParserConstants.DefaultChapter; + chapter.MinNumber = ParserConstants.DefaultChapterNumber; + chapter.MaxNumber = ParserConstants.DefaultChapterNumber; } dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory() diff --git a/API/Data/ManualMigrations/v0.8.0/MigrateChapterNumber.cs b/Kavita.Server/ManualMigrations/v0.8.0/MigrateChapterNumber.cs similarity index 80% rename from API/Data/ManualMigrations/v0.8.0/MigrateChapterNumber.cs rename to Kavita.Server/ManualMigrations/v0.8.0/MigrateChapterNumber.cs index 756055bb7..09c156bb3 100644 --- a/API/Data/ManualMigrations/v0.8.0/MigrateChapterNumber.cs +++ b/Kavita.Server/ManualMigrations/v0.8.0/MigrateChapterNumber.cs @@ -1,12 +1,15 @@ using System; using System.Threading.Tasks; -using API.Entities.History; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Common.Constants; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Constants; +using Kavita.Models.Entities.History; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._0; /// /// Introduced in v0.8.0, this migrates the existing Chapter Range -> Chapter Min/Max Number @@ -28,8 +31,8 @@ public static class MigrateChapterNumber { if (chapter.IsSpecial) { - chapter.MinNumber = Parser.DefaultChapterNumber; - chapter.MaxNumber = Parser.DefaultChapterNumber; + chapter.MinNumber = ParserConstants.DefaultChapterNumber; + chapter.MaxNumber = ParserConstants.DefaultChapterNumber; continue; } chapter.MinNumber = Parser.MinNumberFromRange(chapter.Range); diff --git a/API/Data/ManualMigrations/v0.8.0/MigrateChapterRange.cs b/Kavita.Server/ManualMigrations/v0.8.0/MigrateChapterRange.cs similarity index 87% rename from API/Data/ManualMigrations/v0.8.0/MigrateChapterRange.cs rename to Kavita.Server/ManualMigrations/v0.8.0/MigrateChapterRange.cs index 63e8b889d..352097da9 100644 --- a/API/Data/ManualMigrations/v0.8.0/MigrateChapterRange.cs +++ b/Kavita.Server/ManualMigrations/v0.8.0/MigrateChapterRange.cs @@ -1,13 +1,16 @@ using System; using System.Threading.Tasks; -using API.Entities.History; -using API.Extensions; -using API.Services.Tasks.Scanner.Parser; +using Kavita.API.Database; using Kavita.Common.EnvironmentInfo; +using Kavita.Common.Extensions; +using Kavita.Database; +using Kavita.Models.Entities.History; +using Kavita.Services.Extensions; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._0; /// /// v0.8.0 changed the range to that it doesn't have filename by default diff --git a/API/Data/ManualMigrations/v0.8.0/MigrateCollectionTagToUserCollections.cs b/Kavita.Server/ManualMigrations/v0.8.0/MigrateCollectionTagToUserCollections.cs similarity index 91% rename from API/Data/ManualMigrations/v0.8.0/MigrateCollectionTagToUserCollections.cs rename to Kavita.Server/ManualMigrations/v0.8.0/MigrateCollectionTagToUserCollections.cs index e29e706d0..784223ea4 100644 --- a/API/Data/ManualMigrations/v0.8.0/MigrateCollectionTagToUserCollections.cs +++ b/Kavita.Server/ManualMigrations/v0.8.0/MigrateCollectionTagToUserCollections.cs @@ -1,16 +1,18 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Data.Repositories; -using API.Entities; -using API.Entities.Enums; -using API.Entities.History; -using API.Extensions.QueryExtensions; +using Kavita.API.Database; +using Kavita.API.Repositories; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Database.Extensions; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.History; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._0; /// /// v0.8.0 refactored User Collections diff --git a/API/Data/ManualMigrations/v0.8.0/MigrateDuplicateDarkTheme.cs b/Kavita.Server/ManualMigrations/v0.8.0/MigrateDuplicateDarkTheme.cs similarity index 95% rename from API/Data/ManualMigrations/v0.8.0/MigrateDuplicateDarkTheme.cs rename to Kavita.Server/ManualMigrations/v0.8.0/MigrateDuplicateDarkTheme.cs index 32b4d0fbf..af6743f92 100644 --- a/API/Data/ManualMigrations/v0.8.0/MigrateDuplicateDarkTheme.cs +++ b/Kavita.Server/ManualMigrations/v0.8.0/MigrateDuplicateDarkTheme.cs @@ -1,12 +1,13 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._0; /// /// v0.8.0 ensured that MangaFile Path is normalized. This will normalize existing data to avoid churn. diff --git a/API/Data/ManualMigrations/v0.8.0/MigrateMangaFilePath.cs b/Kavita.Server/ManualMigrations/v0.8.0/MigrateMangaFilePath.cs similarity index 90% rename from API/Data/ManualMigrations/v0.8.0/MigrateMangaFilePath.cs rename to Kavita.Server/ManualMigrations/v0.8.0/MigrateMangaFilePath.cs index 5cbb846af..8b3de40be 100644 --- a/API/Data/ManualMigrations/v0.8.0/MigrateMangaFilePath.cs +++ b/Kavita.Server/ManualMigrations/v0.8.0/MigrateMangaFilePath.cs @@ -1,12 +1,13 @@ using System; using System.Threading.Tasks; -using API.Entities.History; -using API.Services.Tasks.Scanner.Parser; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._0; /// /// v0.8.0 ensured that MangaFile Path is normalized. This will normalize existing data to avoid churn. diff --git a/API/Data/ManualMigrations/v0.8.0/MigrateProgressExport.cs b/Kavita.Server/ManualMigrations/v0.8.0/MigrateProgressExport.cs similarity index 97% rename from API/Data/ManualMigrations/v0.8.0/MigrateProgressExport.cs rename to Kavita.Server/ManualMigrations/v0.8.0/MigrateProgressExport.cs index 498dbb8cc..2d5145fa0 100644 --- a/API/Data/ManualMigrations/v0.8.0/MigrateProgressExport.cs +++ b/Kavita.Server/ManualMigrations/v0.8.0/MigrateProgressExport.cs @@ -3,15 +3,16 @@ using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; -using API.Services; using CsvHelper; using CsvHelper.Configuration.Attributes; +using Kavita.API.Services; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._0; public class ProgressExport { diff --git a/API/Data/ManualMigrations/v0.8.1/MigrateLowestSeriesFolderPath.cs b/Kavita.Server/ManualMigrations/v0.8.1/MigrateLowestSeriesFolderPath.cs similarity index 92% rename from API/Data/ManualMigrations/v0.8.1/MigrateLowestSeriesFolderPath.cs rename to Kavita.Server/ManualMigrations/v0.8.1/MigrateLowestSeriesFolderPath.cs index 02c5b1b92..9aede70d1 100644 --- a/API/Data/ManualMigrations/v0.8.1/MigrateLowestSeriesFolderPath.cs +++ b/Kavita.Server/ManualMigrations/v0.8.1/MigrateLowestSeriesFolderPath.cs @@ -1,12 +1,14 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; +using Kavita.API.Database; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._1; /// /// v0.8.0 released with a bug around LowestSeriesPath. This resets it for all users. diff --git a/API/Data/ManualMigrations/v0.8.2/ManualMigrateSwitchToWal.cs b/Kavita.Server/ManualMigrations/v0.8.2/ManualMigrateSwitchToWal.cs similarity index 93% rename from API/Data/ManualMigrations/v0.8.2/ManualMigrateSwitchToWal.cs rename to Kavita.Server/ManualMigrations/v0.8.2/ManualMigrateSwitchToWal.cs index 9d5e4c59f..3a971c317 100644 --- a/API/Data/ManualMigrations/v0.8.2/ManualMigrateSwitchToWal.cs +++ b/Kavita.Server/ManualMigrations/v0.8.2/ManualMigrateSwitchToWal.cs @@ -1,11 +1,12 @@ using System; using System.Threading.Tasks; -using API.Entities.History; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._2; /// /// v0.8.2 switches Default Kavita installs to WAL diff --git a/API/Data/ManualMigrations/v0.8.2/ManualMigrateThemeDescription.cs b/Kavita.Server/ManualMigrations/v0.8.2/ManualMigrateThemeDescription.cs similarity index 87% rename from API/Data/ManualMigrations/v0.8.2/ManualMigrateThemeDescription.cs rename to Kavita.Server/ManualMigrations/v0.8.2/ManualMigrateThemeDescription.cs index cdbe08287..eaf13ec30 100644 --- a/API/Data/ManualMigrations/v0.8.2/ManualMigrateThemeDescription.cs +++ b/Kavita.Server/ManualMigrations/v0.8.2/ManualMigrateThemeDescription.cs @@ -1,12 +1,14 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._2; /// /// v0.8.2 introduced Theme repo viewer, this adds Description to existing SiteTheme defaults @@ -25,7 +27,7 @@ public static class ManualMigrateThemeDescription var theme = await context.SiteTheme.FirstOrDefaultAsync(t => t.Name == "Dark"); if (theme != null) { - theme.Description = Seed.DefaultThemes.First().Description; + theme.Description = Defaults.DefaultThemes.First().Description; } if (context.ChangeTracker.HasChanges()) diff --git a/API/Data/ManualMigrations/v0.8.2/MigrateInitialInstallData.cs b/Kavita.Server/ManualMigrations/v0.8.2/MigrateInitialInstallData.cs similarity index 91% rename from API/Data/ManualMigrations/v0.8.2/MigrateInitialInstallData.cs rename to Kavita.Server/ManualMigrations/v0.8.2/MigrateInitialInstallData.cs index f1ccea6cf..33504c577 100644 --- a/API/Data/ManualMigrations/v0.8.2/MigrateInitialInstallData.cs +++ b/Kavita.Server/ManualMigrations/v0.8.2/MigrateInitialInstallData.cs @@ -3,14 +3,15 @@ using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Entities.Enums; -using API.Entities.History; -using API.Services; +using Kavita.API.Services; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._2; /// /// v0.8.2 I started collecting information on when the user first installed Kavita as a nice to have info for the user diff --git a/API/Data/ManualMigrations/v0.8.2/MigrateSeriesLowestFolderPath.cs b/Kavita.Server/ManualMigrations/v0.8.2/MigrateSeriesLowestFolderPath.cs similarity index 92% rename from API/Data/ManualMigrations/v0.8.2/MigrateSeriesLowestFolderPath.cs rename to Kavita.Server/ManualMigrations/v0.8.2/MigrateSeriesLowestFolderPath.cs index 2db296444..c2116a740 100644 --- a/API/Data/ManualMigrations/v0.8.2/MigrateSeriesLowestFolderPath.cs +++ b/Kavita.Server/ManualMigrations/v0.8.2/MigrateSeriesLowestFolderPath.cs @@ -1,14 +1,15 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using Kavita.API.Services; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._2; #nullable enable /// diff --git a/API/Data/ManualMigrations/v0.8.4/ManualMigrateEncodeSettings.cs b/Kavita.Server/ManualMigrations/v0.8.4/ManualMigrateEncodeSettings.cs similarity index 93% rename from API/Data/ManualMigrations/v0.8.4/ManualMigrateEncodeSettings.cs rename to Kavita.Server/ManualMigrations/v0.8.4/ManualMigrateEncodeSettings.cs index f9e94836e..a3167c3bf 100644 --- a/API/Data/ManualMigrations/v0.8.4/ManualMigrateEncodeSettings.cs +++ b/Kavita.Server/ManualMigrations/v0.8.4/ManualMigrateEncodeSettings.cs @@ -1,13 +1,14 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using API.Entities.Enums; -using API.Entities.History; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._4; /// /// At some point, encoding settings wrote bad data to the backend, maybe in v0.8.0. This just fixes any bad data. diff --git a/API/Data/ManualMigrations/v0.8.4/ManualMigrateRemovePeople.cs b/Kavita.Server/ManualMigrations/v0.8.4/ManualMigrateRemovePeople.cs similarity index 93% rename from API/Data/ManualMigrations/v0.8.4/ManualMigrateRemovePeople.cs rename to Kavita.Server/ManualMigrations/v0.8.4/ManualMigrateRemovePeople.cs index 2ae22ff52..e0a38a484 100644 --- a/API/Data/ManualMigrations/v0.8.4/ManualMigrateRemovePeople.cs +++ b/Kavita.Server/ManualMigrations/v0.8.4/ManualMigrateRemovePeople.cs @@ -1,11 +1,12 @@ using System; using System.Threading.Tasks; -using API.Entities.History; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._4; /// /// Due to a bug in the initial merge of People/Scanner rework, people got messed up bad. This migration will clear out the table only for nightly users: 0.8.3.15/0.8.3.16 diff --git a/API/Data/ManualMigrations/v0.8.4/ManualMigrateUnscrobbleBookLibraries.cs b/Kavita.Server/ManualMigrations/v0.8.4/ManualMigrateUnscrobbleBookLibraries.cs similarity index 91% rename from API/Data/ManualMigrations/v0.8.4/ManualMigrateUnscrobbleBookLibraries.cs rename to Kavita.Server/ManualMigrations/v0.8.4/ManualMigrateUnscrobbleBookLibraries.cs index 452ca5d09..072000a07 100644 --- a/API/Data/ManualMigrations/v0.8.4/ManualMigrateUnscrobbleBookLibraries.cs +++ b/Kavita.Server/ManualMigrations/v0.8.4/ManualMigrateUnscrobbleBookLibraries.cs @@ -1,13 +1,14 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.Enums; -using API.Entities.History; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._4; /// /// When I removed Scrobble support for Book libraries, I forgot to turn the setting off for said libraries. diff --git a/API/Data/ManualMigrations/v0.8.4/MigrateLowestSeriesFolderPath2.cs b/Kavita.Server/ManualMigrations/v0.8.4/MigrateLowestSeriesFolderPath2.cs similarity index 92% rename from API/Data/ManualMigrations/v0.8.4/MigrateLowestSeriesFolderPath2.cs rename to Kavita.Server/ManualMigrations/v0.8.4/MigrateLowestSeriesFolderPath2.cs index 16a1d7a1a..e7aa5c990 100644 --- a/API/Data/ManualMigrations/v0.8.4/MigrateLowestSeriesFolderPath2.cs +++ b/Kavita.Server/ManualMigrations/v0.8.4/MigrateLowestSeriesFolderPath2.cs @@ -1,12 +1,14 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; +using Kavita.API.Database; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._4; /// /// v0.8.3 still had a bug around LowestSeriesPath. This resets it for all users. diff --git a/API/Data/ManualMigrations/v0.8.5/ManualMigrateBlacklistTableToSeries.cs b/Kavita.Server/ManualMigrations/v0.8.5/ManualMigrateBlacklistTableToSeries.cs similarity index 90% rename from API/Data/ManualMigrations/v0.8.5/ManualMigrateBlacklistTableToSeries.cs rename to Kavita.Server/ManualMigrations/v0.8.5/ManualMigrateBlacklistTableToSeries.cs index 9398b43ab..56fd3567f 100644 --- a/API/Data/ManualMigrations/v0.8.5/ManualMigrateBlacklistTableToSeries.cs +++ b/Kavita.Server/ManualMigrations/v0.8.5/ManualMigrateBlacklistTableToSeries.cs @@ -1,13 +1,15 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; -using API.Entities.Metadata; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Database.Migrations; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using ExternalSeriesMetadata = Kavita.Models.Entities.Metadata.ExternalSeriesMetadata; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._5; /// /// v0.8.5 - Migrating Kavita+ BlacklistedSeries table to Series entity to streamline implementation and generate a "Needs Manual Match" entry for the Series diff --git a/API/Data/ManualMigrations/v0.8.5/ManualMigrateInvalidBlacklistSeries.cs b/Kavita.Server/ManualMigrations/v0.8.5/ManualMigrateInvalidBlacklistSeries.cs similarity index 94% rename from API/Data/ManualMigrations/v0.8.5/ManualMigrateInvalidBlacklistSeries.cs rename to Kavita.Server/ManualMigrations/v0.8.5/ManualMigrateInvalidBlacklistSeries.cs index 7869b4235..25f27abb2 100644 --- a/API/Data/ManualMigrations/v0.8.5/ManualMigrateInvalidBlacklistSeries.cs +++ b/Kavita.Server/ManualMigrations/v0.8.5/ManualMigrateInvalidBlacklistSeries.cs @@ -1,12 +1,13 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._5; /// /// v0.8.5 - Migrating Kavita+ Series that are Blacklisted but have valid ExternalSeries row diff --git a/API/Data/ManualMigrations/v0.8.5/ManualMigrateNeedsManualMatch.cs b/Kavita.Server/ManualMigrations/v0.8.5/ManualMigrateNeedsManualMatch.cs similarity index 90% rename from API/Data/ManualMigrations/v0.8.5/ManualMigrateNeedsManualMatch.cs rename to Kavita.Server/ManualMigrations/v0.8.5/ManualMigrateNeedsManualMatch.cs index bbc4dc593..7715cac08 100644 --- a/API/Data/ManualMigrations/v0.8.5/ManualMigrateNeedsManualMatch.cs +++ b/Kavita.Server/ManualMigrations/v0.8.5/ManualMigrateNeedsManualMatch.cs @@ -1,13 +1,14 @@ using System; using System.Threading.Tasks; -using API.DTOs.KavitaPlus.Manage; -using API.Entities.History; -using API.Extensions.QueryExtensions; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Database.Extensions; +using Kavita.Models.DTOs.KavitaPlus.Manage; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._5; /// /// v0.8.5 - After user testing, the needs manual match has some edge cases from migrations and for best user experience, diff --git a/API/Data/ManualMigrations/v0.8.5/ManualMigrateScrobbleErrors.cs b/Kavita.Server/ManualMigrations/v0.8.5/ManualMigrateScrobbleErrors.cs similarity index 94% rename from API/Data/ManualMigrations/v0.8.5/ManualMigrateScrobbleErrors.cs rename to Kavita.Server/ManualMigrations/v0.8.5/ManualMigrateScrobbleErrors.cs index b0d483de6..7d6facfa2 100644 --- a/API/Data/ManualMigrations/v0.8.5/ManualMigrateScrobbleErrors.cs +++ b/Kavita.Server/ManualMigrations/v0.8.5/ManualMigrateScrobbleErrors.cs @@ -1,12 +1,13 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._5; /// /// v0.8.5 - There seems to be some scrobble events that are pre-scrobble error table that can be processed over and over. diff --git a/API/Data/ManualMigrations/v0.8.5/MigrateProgressExport.cs b/Kavita.Server/ManualMigrations/v0.8.5/MigrateProgressExport.cs similarity index 96% rename from API/Data/ManualMigrations/v0.8.5/MigrateProgressExport.cs rename to Kavita.Server/ManualMigrations/v0.8.5/MigrateProgressExport.cs index e0175fbf3..0e776a904 100644 --- a/API/Data/ManualMigrations/v0.8.5/MigrateProgressExport.cs +++ b/Kavita.Server/ManualMigrations/v0.8.5/MigrateProgressExport.cs @@ -3,14 +3,15 @@ using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; -using API.Services; using CsvHelper; +using Kavita.API.Services; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._5; /// diff --git a/API/Data/ManualMigrations/v0.8.6/ManualMigrateScrobbleEventGen.cs b/Kavita.Server/ManualMigrations/v0.8.6/ManualMigrateScrobbleEventGen.cs similarity index 93% rename from API/Data/ManualMigrations/v0.8.6/ManualMigrateScrobbleEventGen.cs rename to Kavita.Server/ManualMigrations/v0.8.6/ManualMigrateScrobbleEventGen.cs index d0f9421ee..b76fc65a3 100644 --- a/API/Data/ManualMigrations/v0.8.6/ManualMigrateScrobbleEventGen.cs +++ b/Kavita.Server/ManualMigrations/v0.8.6/ManualMigrateScrobbleEventGen.cs @@ -1,12 +1,13 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._6; /// /// v0.8.6 - Manually check when a user triggers scrobble event generation diff --git a/API/Data/ManualMigrations/v0.8.6/ManualMigrateScrobbleSpecials.cs b/Kavita.Server/ManualMigrations/v0.8.6/ManualMigrateScrobbleSpecials.cs similarity index 92% rename from API/Data/ManualMigrations/v0.8.6/ManualMigrateScrobbleSpecials.cs rename to Kavita.Server/ManualMigrations/v0.8.6/ManualMigrateScrobbleSpecials.cs index 4749ff2ec..518ce8318 100644 --- a/API/Data/ManualMigrations/v0.8.6/ManualMigrateScrobbleSpecials.cs +++ b/Kavita.Server/ManualMigrations/v0.8.6/ManualMigrateScrobbleSpecials.cs @@ -1,13 +1,14 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; -using API.Services.Tasks.Scanner.Parser; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._6; /// /// v0.8.6 - Change to not scrobble specials as they will never process, this migration removes all existing scrobble events diff --git a/API/Data/ManualMigrations/v0.8.7/ManualMigrateReadingProfiles.cs b/Kavita.Server/ManualMigrations/v0.8.7/ManualMigrateReadingProfiles.cs similarity index 94% rename from API/Data/ManualMigrations/v0.8.7/ManualMigrateReadingProfiles.cs rename to Kavita.Server/ManualMigrations/v0.8.7/ManualMigrateReadingProfiles.cs index 514ba23ac..e837b6f23 100644 --- a/API/Data/ManualMigrations/v0.8.7/ManualMigrateReadingProfiles.cs +++ b/Kavita.Server/ManualMigrations/v0.8.7/ManualMigrateReadingProfiles.cs @@ -1,14 +1,15 @@ using System; using System.Threading.Tasks; -using API.Entities; -using API.Entities.Enums; -using API.Entities.History; -using API.Extensions; using Kavita.Common.EnvironmentInfo; +using Kavita.Common.Extensions; +using Kavita.Database; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.History; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._7; public static class ManualMigrateReadingProfiles { diff --git a/API/Data/ManualMigrations/v0.8.8/ManualMigrateBookReadingProgress.cs b/Kavita.Server/ManualMigrations/v0.8.8/ManualMigrateBookReadingProgress.cs similarity index 96% rename from API/Data/ManualMigrations/v0.8.8/ManualMigrateBookReadingProgress.cs rename to Kavita.Server/ManualMigrations/v0.8.8/ManualMigrateBookReadingProgress.cs index e03d7de59..4b83067b8 100644 --- a/API/Data/ManualMigrations/v0.8.8/ManualMigrateBookReadingProgress.cs +++ b/Kavita.Server/ManualMigrations/v0.8.8/ManualMigrateBookReadingProgress.cs @@ -1,12 +1,14 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Entities.History; +using Kavita.API.Database; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._8; /// /// v0.8.8 - Switch existing xpaths saved to a descoped version diff --git a/API/Data/ManualMigrations/v0.8.8/ManualMigrateEnableMetadataMatchingDefault.cs b/Kavita.Server/ManualMigrations/v0.8.8/ManualMigrateEnableMetadataMatchingDefault.cs similarity index 93% rename from API/Data/ManualMigrations/v0.8.8/ManualMigrateEnableMetadataMatchingDefault.cs rename to Kavita.Server/ManualMigrations/v0.8.8/ManualMigrateEnableMetadataMatchingDefault.cs index 5bb8aeb94..1e5cb0139 100644 --- a/API/Data/ManualMigrations/v0.8.8/ManualMigrateEnableMetadataMatchingDefault.cs +++ b/Kavita.Server/ManualMigrations/v0.8.8/ManualMigrateEnableMetadataMatchingDefault.cs @@ -1,11 +1,13 @@ using System; using System.Threading.Tasks; -using API.Entities.History; +using Kavita.API.Database; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.History; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._8; /// /// v0.8.8 - If Kavita+ users had Metadata Matching settings already, ensure the new non-Kavita+ system is enabled to match diff --git a/API/Data/ManualMigrations/v0.8.9/MigrateBadKoreaderProgress.cs b/Kavita.Server/ManualMigrations/v0.8.9/MigrateBadKoreaderProgress.cs similarity index 94% rename from API/Data/ManualMigrations/v0.8.9/MigrateBadKoreaderProgress.cs rename to Kavita.Server/ManualMigrations/v0.8.9/MigrateBadKoreaderProgress.cs index 79e73d0ce..46af27039 100644 --- a/API/Data/ManualMigrations/v0.8.9/MigrateBadKoreaderProgress.cs +++ b/Kavita.Server/ManualMigrations/v0.8.9/MigrateBadKoreaderProgress.cs @@ -1,10 +1,10 @@ using System.Linq; using System.Threading.Tasks; -using API.Data.Misc; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._9; /// /// v0.8.8.28 - There was bad code in the nightlies where Progress events with LibraryId = 0 could be saved. This will fix up those events. diff --git a/API/Data/ManualMigrations/v0.8.9/MigrateFormatToActivityData.cs b/Kavita.Server/ManualMigrations/v0.8.9/MigrateFormatToActivityData.cs similarity index 95% rename from API/Data/ManualMigrations/v0.8.9/MigrateFormatToActivityData.cs rename to Kavita.Server/ManualMigrations/v0.8.9/MigrateFormatToActivityData.cs index 1ce4d9340..1e0cbd6b7 100644 --- a/API/Data/ManualMigrations/v0.8.9/MigrateFormatToActivityData.cs +++ b/Kavita.Server/ManualMigrations/v0.8.9/MigrateFormatToActivityData.cs @@ -1,11 +1,11 @@ using System.Linq; using System.Threading.Tasks; -using API.Data.Misc; -using API.Entities.Enums; +using Kavita.Database; +using Kavita.Models.Entities.Enums; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._9; /// /// v0.8.8.16 - Needed to add Format to the ActivityData to optimize a query diff --git a/API/Data/ManualMigrations/v0.8.9/MigrateIncorrectUtcMidnightRollovers.cs b/Kavita.Server/ManualMigrations/v0.8.9/MigrateIncorrectUtcMidnightRollovers.cs similarity index 96% rename from API/Data/ManualMigrations/v0.8.9/MigrateIncorrectUtcMidnightRollovers.cs rename to Kavita.Server/ManualMigrations/v0.8.9/MigrateIncorrectUtcMidnightRollovers.cs index 5cb98846e..3719502b9 100644 --- a/API/Data/ManualMigrations/v0.8.9/MigrateIncorrectUtcMidnightRollovers.cs +++ b/Kavita.Server/ManualMigrations/v0.8.9/MigrateIncorrectUtcMidnightRollovers.cs @@ -1,11 +1,11 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Data.Misc; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._9; public class MigrateIncorrectUtcTimes: ManualMigration { diff --git a/API/Data/ManualMigrations/v0.8.9/MigrateMissingAppUserRatingDateColumns.cs b/Kavita.Server/ManualMigrations/v0.8.9/MigrateMissingAppUserRatingDateColumns.cs similarity index 98% rename from API/Data/ManualMigrations/v0.8.9/MigrateMissingAppUserRatingDateColumns.cs rename to Kavita.Server/ManualMigrations/v0.8.9/MigrateMissingAppUserRatingDateColumns.cs index b3462560a..c8393e04b 100644 --- a/API/Data/ManualMigrations/v0.8.9/MigrateMissingAppUserRatingDateColumns.cs +++ b/Kavita.Server/ManualMigrations/v0.8.9/MigrateMissingAppUserRatingDateColumns.cs @@ -2,11 +2,11 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Data.Misc; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._9; /// /// v0.8.9 - AppUserRating is missing Created/CreatedUtc/LastModified/LastModifiedUtc on old installs diff --git a/API/Data/ManualMigrations/v0.8.9/MigrateMissingCreatedUtcDate.cs b/Kavita.Server/ManualMigrations/v0.8.9/MigrateMissingCreatedUtcDate.cs similarity index 90% rename from API/Data/ManualMigrations/v0.8.9/MigrateMissingCreatedUtcDate.cs rename to Kavita.Server/ManualMigrations/v0.8.9/MigrateMissingCreatedUtcDate.cs index c8a664d5e..a61b54aab 100644 --- a/API/Data/ManualMigrations/v0.8.9/MigrateMissingCreatedUtcDate.cs +++ b/Kavita.Server/ManualMigrations/v0.8.9/MigrateMissingCreatedUtcDate.cs @@ -1,13 +1,11 @@ using System; using System.Linq; using System.Threading.Tasks; -using API.Data.Misc; -using API.Entities.History; -using Kavita.Common.EnvironmentInfo; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._9; /// /// v0.8.9 - Some AppUser rows are missing CreatedUtc date diff --git a/API/Data/ManualMigrations/v0.8.9/MigrateProgressToReadingSessions.cs b/Kavita.Server/ManualMigrations/v0.8.9/MigrateProgressToReadingSessions.cs similarity index 96% rename from API/Data/ManualMigrations/v0.8.9/MigrateProgressToReadingSessions.cs rename to Kavita.Server/ManualMigrations/v0.8.9/MigrateProgressToReadingSessions.cs index 1a7e17468..fa89ff655 100644 --- a/API/Data/ManualMigrations/v0.8.9/MigrateProgressToReadingSessions.cs +++ b/Kavita.Server/ManualMigrations/v0.8.9/MigrateProgressToReadingSessions.cs @@ -2,17 +2,15 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Data.Misc; -using API.Entities; -using API.Entities.Enums; -using API.Entities.History; -using API.Entities.Progress; -using API.Services.Reading; -using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; +using Kavita.Services.Reading; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._9; /// diff --git a/API/Data/ManualMigrations/v0.8.9/MigrateToAuthKeys.cs b/Kavita.Server/ManualMigrations/v0.8.9/MigrateToAuthKeys.cs similarity index 87% rename from API/Data/ManualMigrations/v0.8.9/MigrateToAuthKeys.cs rename to Kavita.Server/ManualMigrations/v0.8.9/MigrateToAuthKeys.cs index 4009609a3..83f56f8c9 100644 --- a/API/Data/ManualMigrations/v0.8.9/MigrateToAuthKeys.cs +++ b/Kavita.Server/ManualMigrations/v0.8.9/MigrateToAuthKeys.cs @@ -1,16 +1,13 @@ using System; -using System.Linq; using System.Threading.Tasks; -using API.Data.Misc; -using API.Entities.Enums.User; -using API.Entities.History; -using API.Entities.User; -using API.Helpers; -using Kavita.Common.EnvironmentInfo; +using Kavita.Common.Helpers; +using Kavita.Database; +using Kavita.Models.Entities.Enums.User; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._9; /// /// v0.8.9 - Migrating from fixed api key to user-defined with configurable length @@ -29,7 +26,7 @@ public class MigrateToAuthKeys : ManualMigration foreach (var user in allUsers) { if (user.AuthKeys.Count != 0) continue; - + var key = new AppUserAuthKey() { Name = AuthKeyHelper.OpdsKeyName, diff --git a/API/Data/ManualMigrations/v0.8.9/MigrateTotalReads.cs b/Kavita.Server/ManualMigrations/v0.8.9/MigrateTotalReads.cs similarity index 89% rename from API/Data/ManualMigrations/v0.8.9/MigrateTotalReads.cs rename to Kavita.Server/ManualMigrations/v0.8.9/MigrateTotalReads.cs index 418de1f88..a2eeba5a5 100644 --- a/API/Data/ManualMigrations/v0.8.9/MigrateTotalReads.cs +++ b/Kavita.Server/ManualMigrations/v0.8.9/MigrateTotalReads.cs @@ -1,15 +1,10 @@ -using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Data.Misc; -using API.Entities.History; -using API.Entities.Progress; -using Kavita.Common.EnvironmentInfo; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Data.ManualMigrations; +namespace Kavita.Server.ManualMigrations.v0._8._9; /// /// v0.8.9 - This migrates all records that diff --git a/API/Middleware/AuthKeyAuthenticationHandler.cs b/Kavita.Server/Middleware/AuthKeyAuthenticationHandler.cs similarity index 96% rename from API/Middleware/AuthKeyAuthenticationHandler.cs rename to Kavita.Server/Middleware/AuthKeyAuthenticationHandler.cs index bb0bc6c3a..913e0ca4e 100644 --- a/API/Middleware/AuthKeyAuthenticationHandler.cs +++ b/Kavita.Server/Middleware/AuthKeyAuthenticationHandler.cs @@ -5,11 +5,11 @@ using System.Linq; using System.Security.Claims; using System.Text.Encodings.Web; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Entities.Progress; -using API.Services; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.Common.Constants; +using Kavita.Models.Entities.Progress; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Caching.Hybrid; @@ -17,8 +17,7 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace API.Middleware; -#nullable enable +namespace Kavita.Server.Middleware; public class AuthKeyAuthenticationOptions : AuthenticationSchemeOptions { diff --git a/API/Middleware/RateLimit/AuthenticationRateLimiterPolicy.cs b/Kavita.Server/Middleware/AuthenticationRateLimiterPolicy.cs similarity index 95% rename from API/Middleware/RateLimit/AuthenticationRateLimiterPolicy.cs rename to Kavita.Server/Middleware/AuthenticationRateLimiterPolicy.cs index c2119bb13..dc5c0a4ac 100644 --- a/API/Middleware/RateLimit/AuthenticationRateLimiterPolicy.cs +++ b/Kavita.Server/Middleware/AuthenticationRateLimiterPolicy.cs @@ -6,8 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.RateLimiting; -namespace API.Middleware.RateLimit; -#nullable enable +namespace Kavita.Server.Middleware; public class AuthenticationRateLimiterPolicy : IRateLimiterPolicy { diff --git a/API/Services/Reading/ClientInfoAccessor.cs b/Kavita.Server/Middleware/ClientInfoAccessor.cs similarity index 66% rename from API/Services/Reading/ClientInfoAccessor.cs rename to Kavita.Server/Middleware/ClientInfoAccessor.cs index defaeb9c6..f3de2cf19 100644 --- a/API/Services/Reading/ClientInfoAccessor.cs +++ b/Kavita.Server/Middleware/ClientInfoAccessor.cs @@ -1,28 +1,9 @@ using System.Threading; -using API.Entities.Progress; -using API.Entities.User; +using Kavita.API.Services; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; -namespace API.Services.Reading; -#nullable enable - -/// -/// Provides access to client information for the current request. -/// This service captures details about the client making the request including -/// browser info, device type, authentication method, etc. -/// -public interface IClientInfoAccessor -{ - /// - /// Gets the client information for the current request. - /// Returns null if called outside an HTTP request context (e.g., background jobs). - /// - ClientInfoData? Current { get; } - string? CurrentUiFingerprint { get; } - /// - /// Client Device PK - /// - int? CurrentDeviceId { get; } -} +namespace Kavita.Server.Middleware; /// /// Thread-safe accessor for client information using AsyncLocal storage. diff --git a/API/Middleware/ClientInfoMiddleware.cs b/Kavita.Server/Middleware/ClientInfoMiddleware.cs similarity index 95% rename from API/Middleware/ClientInfoMiddleware.cs rename to Kavita.Server/Middleware/ClientInfoMiddleware.cs index ae34e0125..61015ec42 100644 --- a/API/Middleware/ClientInfoMiddleware.cs +++ b/Kavita.Server/Middleware/ClientInfoMiddleware.cs @@ -2,18 +2,16 @@ using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; -using API.Constants; -using API.Entities.Enums; -using API.Entities.Progress; -using API.Extensions; -using API.Helpers; -using API.Services.Reading; -using API.Services.Store; +using Kavita.API.Store; +using Kavita.Common.Constants; +using Kavita.Common.Extensions; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; +using Kavita.Server.Helpers; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; -namespace API.Middleware; +namespace Kavita.Server.Middleware; /// diff --git a/API/Middleware/DeviceTrackingMiddleware.cs b/Kavita.Server/Middleware/DeviceTrackingMiddleware.cs similarity index 81% rename from API/Middleware/DeviceTrackingMiddleware.cs rename to Kavita.Server/Middleware/DeviceTrackingMiddleware.cs index c32d93048..a38b968de 100644 --- a/API/Middleware/DeviceTrackingMiddleware.cs +++ b/Kavita.Server/Middleware/DeviceTrackingMiddleware.cs @@ -1,14 +1,12 @@ using System; -using System.Diagnostics; using System.Threading.Tasks; -using API.Services; -using API.Services.Reading; -using API.Services.Store; +using Kavita.API.Attributes; +using Kavita.API.Services; +using Kavita.API.Store; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -namespace API.Middleware; -#nullable enable +namespace Kavita.Server.Middleware; /// /// Middleware that identifies and tracks device activity for authenticated requests. @@ -63,10 +61,3 @@ public class DeviceTrackingMiddleware(RequestDelegate next, ILogger -/// Attribute to skip device tracking on specific endpoints. -/// Use for high-frequency endpoints where device tracking adds unnecessary overhead. -/// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public class SkipDeviceTrackingAttribute : Attribute; diff --git a/API/Middleware/ExceptionMiddleware.cs b/Kavita.Server/Middleware/ExceptionMiddleware.cs similarity index 89% rename from API/Middleware/ExceptionMiddleware.cs rename to Kavita.Server/Middleware/ExceptionMiddleware.cs index ec8418cf0..9d1e0cf77 100644 --- a/API/Middleware/ExceptionMiddleware.cs +++ b/Kavita.Server/Middleware/ExceptionMiddleware.cs @@ -2,12 +2,12 @@ using System.Net; using System.Text.Json; using System.Threading.Tasks; -using API.Errors; +using Kavita.API.Errors; using Kavita.Common; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -namespace API.Middleware; +namespace Kavita.Server.Middleware; public class ExceptionMiddleware(RequestDelegate next, ILogger logger) { @@ -22,7 +22,7 @@ public class ExceptionMiddleware(RequestDelegate next, ILogger /// If the user is authenticated, will update the field. diff --git a/API/Middleware/UserContextMiddleware.cs b/Kavita.Server/Middleware/UserContextMiddleware.cs similarity index 91% rename from API/Middleware/UserContextMiddleware.cs rename to Kavita.Server/Middleware/UserContextMiddleware.cs index 1881c9fba..afba1939c 100644 --- a/API/Middleware/UserContextMiddleware.cs +++ b/Kavita.Server/Middleware/UserContextMiddleware.cs @@ -3,14 +3,12 @@ using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; -using API.Entities.Progress; -using API.Services; -using API.Services.Store; +using Kavita.Models.Entities.Progress; +using Kavita.Server.Store; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -namespace API.Middleware; -#nullable enable +namespace Kavita.Server.Middleware; /// /// Middleware that resolves user identity from various authentication methods @@ -19,9 +17,7 @@ namespace API.Middleware; /// public class UserContextMiddleware(RequestDelegate next, ILogger logger) { - public async Task InvokeAsync( - HttpContext context, - UserContext userContext) + public async Task InvokeAsync(HttpContext context, UserContext userContext) { try { diff --git a/API/Program.cs b/Kavita.Server/Program.cs similarity index 95% rename from API/Program.cs rename to Kavita.Server/Program.cs index 15dad328e..98a61cf6d 100644 --- a/API/Program.cs +++ b/Kavita.Server/Program.cs @@ -4,15 +4,19 @@ using System.IO.Abstractions; using System.Linq; using System.Security.Cryptography; using System.Threading.Tasks; -using API.Data; -using API.Data.ManualMigrations; -using API.Entities; -using API.Entities.Enums; -using API.Logging; -using API.Services; -using API.SignalR; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Server.Logging; +using Kavita.Server.ManualMigrations.v0._7._14; +using Kavita.Server.ManualMigrations.v0._8._2; +using Kavita.Server.ManualMigrations.v0._8._4; +using Kavita.Services; +using Kavita.Services.SignalR; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Server.Kestrel.Core; @@ -24,12 +28,11 @@ using Microsoft.Extensions.Logging; using NetVips; using Serilog; using Serilog.Events; +using Serilog.Formatting.Display; using Serilog.Sinks.AspNetCore.SignalR.Extensions; using Log = Serilog.Log; -using MessageTemplateTextFormatter = Serilog.Formatting.Display.MessageTemplateTextFormatter; -namespace API; -#nullable enable +namespace Kavita.Server; public class Program { diff --git a/Kavita.Server/Properties/launchSettings.json b/Kavita.Server/Properties/launchSettings.json new file mode 100644 index 000000000..66da4a630 --- /dev/null +++ b/Kavita.Server/Properties/launchSettings.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:14778", + "sslPort": 44368 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Server": { + "workingDirectory": ".", + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": false, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/API/Startup.cs b/Kavita.Server/Startup.cs similarity index 94% rename from API/Startup.cs rename to Kavita.Server/Startup.cs index 368a86816..1fafdeea8 100644 --- a/API/Startup.cs +++ b/Kavita.Server/Startup.cs @@ -7,23 +7,34 @@ using System.Net; using System.Net.Sockets; using System.Reflection; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.ManualMigrations; -using API.DTOs.Internal; -using API.Entities.Enums; -using API.Extensions; -using API.Logging; -using API.Middleware; -using API.Middleware.RateLimit; -using API.Services; -using API.Services.HostedServices; -using API.Services.Tasks; -using API.SignalR; using Hangfire; using HtmlAgilityPack; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Common.Constants; using Kavita.Common.EnvironmentInfo; +using Kavita.Database; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Internal; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Extensions; +using Kavita.Server.Extensions; +using Kavita.Server.Logging; +using Kavita.Server.ManualMigrations.v0._7._11; +using Kavita.Server.ManualMigrations.v0._7._14; +using Kavita.Server.ManualMigrations.v0._7._9; +using Kavita.Server.ManualMigrations.v0._8._0; +using Kavita.Server.ManualMigrations.v0._8._1; +using Kavita.Server.ManualMigrations.v0._8._2; +using Kavita.Server.ManualMigrations.v0._8._4; +using Kavita.Server.ManualMigrations.v0._8._5; +using Kavita.Server.ManualMigrations.v0._8._6; +using Kavita.Server.ManualMigrations.v0._8._7; +using Kavita.Server.ManualMigrations.v0._8._8; +using Kavita.Server.ManualMigrations.v0._8._9; +using Kavita.Server.Middleware; +using Kavita.Services.SignalR; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Hosting; @@ -40,9 +51,9 @@ using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; using Microsoft.OpenApi; using Serilog; -using TaskScheduler = API.Services.TaskScheduler; +using TaskScheduler = Kavita.Services.TaskScheduler; -namespace API; +namespace Kavita.Server; public class Startup { @@ -62,13 +73,9 @@ public class Startup public void ConfigureServices(IServiceCollection services) { services.Configure(_config); + services.AddMappings(); services.AddApplicationServices(_config, _env); - // Store keys inside database, such that cookies can be decrypted between container restarts - services.AddDataProtection() - .PersistKeysToDbContext() - .SetApplicationName(BuildInfo.AppName); - services.AddControllers(options => { options.CacheProfiles.Add(ResponseCacheProfiles.Minute, @@ -193,13 +200,8 @@ public class Startup // Add the processing server as IHostedService services.AddHangfireServer(options => { - options.Queues = [TaskScheduler.ScanQueue, TaskScheduler.DefaultQueue]; + options.Queues = [TaskSchedulerConstants.ScanQueue, TaskSchedulerConstants.DefaultQueue]; }); - - // Add IHostedService for startup tasks - // Any services that should be bootstrapped go here - services.AddHostedService(); - services.AddHostedService(); } private static void AddCompressionAndCaching(IServiceCollection services) @@ -244,7 +246,6 @@ public class Startup app.UseMiddleware(); app.UseMiddleware(); - if (env.IsDevelopment()) { app.UseSwagger(); diff --git a/API/Services/Store/CustomTicketStore.cs b/Kavita.Server/Store/CustomTicketStore.cs similarity index 98% rename from API/Services/Store/CustomTicketStore.cs rename to Kavita.Server/Store/CustomTicketStore.cs index 13a57af78..3b73dfed0 100644 --- a/API/Services/Store/CustomTicketStore.cs +++ b/Kavita.Server/Store/CustomTicketStore.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.Extensions.Caching.Distributed; -namespace API.Services.Store; +namespace Kavita.Server.Store; /// /// The is used as for the OIDC implementation diff --git a/Kavita.Server/Store/UserContext.cs b/Kavita.Server/Store/UserContext.cs new file mode 100644 index 000000000..e527286ab --- /dev/null +++ b/Kavita.Server/Store/UserContext.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Kavita.API.Store; +using Kavita.Common; +using Kavita.Models.Entities.Progress; + +namespace Kavita.Server.Store; + +public class UserContext : IUserContext +{ + private int? _userId; + private string? _username; + private AuthenticationType _authType; + private List _roles = new(); + + public int? GetUserId() => _userId; + + public int GetUserIdOrThrow() + { + return _userId ?? throw new UnauthorizedAccessException(); + } + + public string? GetUsername() => _username; + + public AuthenticationType GetAuthenticationType() => _authType; + + public bool IsAuthenticated { get; private set; } + public IReadOnlyList Roles => _roles.AsReadOnly(); + + // Internal method used by middleware to set context + internal void SetUserContext(int userId, string username, AuthenticationType authType, IEnumerable roles) + { + _userId = userId; + _username = username; + _authType = authType; + IsAuthenticated = true; + _roles = roles?.ToList() ?? []; + } + + internal void Clear() + { + _userId = null; + _username = null; + _authType = AuthenticationType.Unknown; + IsAuthenticated = false; + _roles.Clear(); + } + + public bool HasRole(string role) + { + return _roles.Any(r => r.Equals(role, StringComparison.OrdinalIgnoreCase)); + } + + public bool HasAnyRole(params string[] roles) + { + return roles.Any(HasRole); + } + + public bool HasAllRoles(params string[] roles) + { + return roles.All(HasRole); + } +} diff --git a/API/config/appsettings.Development.json b/Kavita.Server/config/appsettings.Development.json similarity index 100% rename from API/config/appsettings.Development.json rename to Kavita.Server/config/appsettings.Development.json diff --git a/API/config/appsettings.json b/Kavita.Server/config/appsettings.json similarity index 100% rename from API/config/appsettings.json rename to Kavita.Server/config/appsettings.json diff --git a/API/config/templates/EmailChange.html b/Kavita.Server/config/templates/EmailChange.html similarity index 100% rename from API/config/templates/EmailChange.html rename to Kavita.Server/config/templates/EmailChange.html diff --git a/API/config/templates/EmailConfirm.html b/Kavita.Server/config/templates/EmailConfirm.html similarity index 100% rename from API/config/templates/EmailConfirm.html rename to Kavita.Server/config/templates/EmailConfirm.html diff --git a/API/config/templates/EmailMigration.html b/Kavita.Server/config/templates/EmailMigration.html similarity index 100% rename from API/config/templates/EmailMigration.html rename to Kavita.Server/config/templates/EmailMigration.html diff --git a/API/config/templates/EmailPasswordReset.html b/Kavita.Server/config/templates/EmailPasswordReset.html similarity index 100% rename from API/config/templates/EmailPasswordReset.html rename to Kavita.Server/config/templates/EmailPasswordReset.html diff --git a/API/config/templates/EmailTest.html b/Kavita.Server/config/templates/EmailTest.html similarity index 100% rename from API/config/templates/EmailTest.html rename to Kavita.Server/config/templates/EmailTest.html diff --git a/API/config/templates/SendToDevice.html b/Kavita.Server/config/templates/SendToDevice.html similarity index 100% rename from API/config/templates/SendToDevice.html rename to Kavita.Server/config/templates/SendToDevice.html diff --git a/API/config/templates/TokenExpiration.html b/Kavita.Server/config/templates/TokenExpiration.html similarity index 100% rename from API/config/templates/TokenExpiration.html rename to Kavita.Server/config/templates/TokenExpiration.html diff --git a/API/config/templates/TokenExpiringSoon.html b/Kavita.Server/config/templates/TokenExpiringSoon.html similarity index 100% rename from API/config/templates/TokenExpiringSoon.html rename to Kavita.Server/config/templates/TokenExpiringSoon.html diff --git a/API.Tests/Services/AccountServiceTests.cs b/Kavita.Services.Tests/AccountServiceTests.cs similarity index 95% rename from API.Tests/Services/AccountServiceTests.cs rename to Kavita.Services.Tests/AccountServiceTests.cs index 4989f4e78..5942ac032 100644 --- a/API.Tests/Services/AccountServiceTests.cs +++ b/Kavita.Services.Tests/AccountServiceTests.cs @@ -1,25 +1,23 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.Entities; -using API.Entities.Enums; -using API.Helpers.Builders; -using API.Services; -using API.Services.Tasks.Scanner; using AutoMapper; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Scanner; using Kavita.Common; +using Kavita.Database; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.Constants; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class AccountServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { @@ -287,9 +285,9 @@ public class AccountServiceTests(ITestOutputHelper outputHelper): AbstractDbTest await userManager.CreateAsync(defaultAdmin); var accountService = new AccountService(userManager, Substitute.For>(), unitOfWork, mapper, Substitute.For()); - var settingsService = new SettingsService(unitOfWork, Substitute.For(), Substitute.For(), Substitute.For(), Substitute.For> (), Substitute.For()); + var settingsService = new SettingsService(unitOfWork, Substitute.For(), Substitute.For(), Substitute.For(), Substitute.For> (), Substitute.For(), Substitute.For()); user = await unitOfWork.UserRepository.GetUserByIdAsync(user.Id, AppUserIncludes.SideNavStreams); - return (user, accountService, userManager, settingsService); + return (user!, accountService, userManager, settingsService); } } diff --git a/API.Tests/Services/AnnotationServiceTests.cs b/Kavita.Services.Tests/AnnotationServiceTests.cs similarity index 94% rename from API.Tests/Services/AnnotationServiceTests.cs rename to Kavita.Services.Tests/AnnotationServiceTests.cs index 3929ab99d..02ebc52e4 100644 --- a/API.Tests/Services/AnnotationServiceTests.cs +++ b/Kavita.Services.Tests/AnnotationServiceTests.cs @@ -1,23 +1,24 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Reader; -using API.DTOs.Settings; -using API.Entities; -using API.Helpers.Builders; -using API.Services; -using API.SignalR; using AutoMapper; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Database; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class AnnotationServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { diff --git a/API.Tests/Services/ArchiveServiceTests.cs b/Kavita.Services.Tests/ArchiveServiceTests.cs similarity index 92% rename from API.Tests/Services/ArchiveServiceTests.cs rename to Kavita.Services.Tests/ArchiveServiceTests.cs index 489dd27ec..538b5be8f 100644 --- a/API.Tests/Services/ArchiveServiceTests.cs +++ b/Kavita.Services.Tests/ArchiveServiceTests.cs @@ -1,20 +1,18 @@ using System.Diagnostics; -using System.IO; using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers; using System.IO.Compression; -using System.Linq; -using API.DTOs.Archive; -using API.Entities.Enums; -using API.Services; +using Kavita.API.Services; +using Kavita.Models.DTOs.Archive; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NetVips; using NSubstitute; using NSubstitute.Extensions; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class ArchiveServiceTests { @@ -39,7 +37,7 @@ public class ArchiveServiceTests [InlineData("file in folder_alt.zip", true)] public void ArchiveNeedsFlatteningTest(string archivePath, bool expected) { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/Archives"); var file = Path.Join(testDirectory, archivePath); using var archive = ZipFile.OpenRead(file); Assert.Equal(expected, _archiveService.ArchiveNeedsFlattening(archive)); @@ -55,7 +53,7 @@ public class ArchiveServiceTests [InlineData("file in folder_alt.zip", true)] public void IsValidArchiveTest(string archivePath, bool expected) { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/Archives"); Assert.Equal(expected, _archiveService.IsValidArchive(Path.Join(testDirectory, archivePath))); } @@ -73,7 +71,7 @@ public class ArchiveServiceTests [InlineData("macos_withdotunder_one.zip", 1)] public void GetNumberOfPagesFromArchiveTest(string archivePath, int expected) { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/Archives"); var sw = Stopwatch.StartNew(); Assert.Equal(expected, _archiveService.GetNumberOfPagesFromArchive(Path.Join(testDirectory, archivePath))); _testOutputHelper.WriteLine($"Processed Original in {sw.ElapsedMilliseconds} ms"); @@ -92,7 +90,7 @@ public class ArchiveServiceTests public void CanOpenArchive(string archivePath, ArchiveLibrary expected) { var sw = Stopwatch.StartNew(); - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/Archives"); Assert.Equal(expected, _archiveService.CanOpen(Path.Join(testDirectory, archivePath))); _testOutputHelper.WriteLine($"Processed Original in {sw.ElapsedMilliseconds} ms"); @@ -110,8 +108,8 @@ public class ArchiveServiceTests public void CanExtractArchive(string archivePath, int expectedFileCount) { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives"); - var extractDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives/Extraction"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/Archives"); + var extractDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/Archives/Extraction"); _directoryService.ClearAndDeleteDirectory(extractDirectory); @@ -169,7 +167,7 @@ public class ArchiveServiceTests var imageService = new ImageService(Substitute.For>(), ds); var archiveService = Substitute.For(_logger, ds, imageService, Substitute.For()); - var testDirectory = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/CoverImages")); + var testDirectory = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/CoverImages")); var expectedBytes = Image.Thumbnail(Path.Join(testDirectory, expectedOutputFile), 320).WriteToBuffer(".png"); archiveService.Configure().CanOpen(Path.Join(testDirectory, inputFile)).Returns(ArchiveLibrary.Default); @@ -201,7 +199,7 @@ public class ArchiveServiceTests var archiveService = Substitute.For(_logger, new DirectoryService(_directoryServiceLogger, new FileSystem()), imageService, Substitute.For()); - var testDirectory = API.Services.Tasks.Scanner.Parser.Parser.NormalizePath(Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/CoverImages"))); + var testDirectory = Parser.NormalizePath(Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/CoverImages"))); var outputDir = Path.Join(testDirectory, "output"); _directoryService.ClearDirectory(outputDir); @@ -226,7 +224,7 @@ public class ArchiveServiceTests imageService.WriteCoverThumbnail(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(x => "cover.jpg"); var archiveService = new ArchiveService(_logger, _directoryService, imageService, Substitute.For()); - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/"); var inputPath = Path.GetFullPath(Path.Join(testDirectory, inputFile)); var outputPath = Path.Join(testDirectory, Path.GetFileNameWithoutExtension(inputFile) + "_output"); new DirectoryInfo(outputPath).Create(); @@ -240,7 +238,7 @@ public class ArchiveServiceTests [Fact] public void ShouldHaveComicInfo() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/ComicInfos"); var archive = Path.Join(testDirectory, "ComicInfo.zip"); const string summaryInfo = "By all counts, Ryouta Sakamoto is a loser when he's not holed up in his room, bombing things into oblivion in his favorite online action RPG. But his very own uneventful life is blown to pieces when he's abducted and taken to an uninhabited island, where he soon learns the hard way that he's being pitted against others just like him in a explosives-riddled death match! How could this be happening? Who's putting them up to this? And why!? The name, not to mention the objective, of this very real survival game is eerily familiar to Ryouta, who has mastered its virtual counterpart-BTOOOM! Can Ryouta still come out on top when he's playing for his life!?"; @@ -252,7 +250,7 @@ public class ArchiveServiceTests [Fact] public void ShouldHaveComicInfo_CanParseUmlaut() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/ComicInfos"); var archive = Path.Join(testDirectory, "Umlaut.zip"); var comicInfo = _archiveService.GetComicInfo(archive); @@ -263,7 +261,7 @@ public class ArchiveServiceTests [Fact] public void ShouldHaveComicInfo_WithAuthors() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/ComicInfos"); var archive = Path.Join(testDirectory, "ComicInfo_authors.zip"); var comicInfo = _archiveService.GetComicInfo(archive); @@ -277,7 +275,7 @@ public class ArchiveServiceTests [InlineData("ComicInfo_duplicateInfos.rar")] public void ShouldHaveComicInfo_TopLevelFileOnly(string filename) { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/ComicInfos"); var archive = Path.Join(testDirectory, filename); var comicInfo = _archiveService.GetComicInfo(archive); @@ -288,7 +286,7 @@ public class ArchiveServiceTests [Fact] public void ShouldHaveComicInfo_OutsideRoot() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/ComicInfos"); var archive = Path.Join(testDirectory, "ComicInfo_outside_root.zip"); var comicInfo = _archiveService.GetComicInfo(archive); @@ -299,7 +297,7 @@ public class ArchiveServiceTests [Fact] public void ShouldHaveComicInfo_OutsideRoot_SharpCompress() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/ComicInfos"); var archive = Path.Join(testDirectory, "ComicInfo_outside_root_SharpCompress.cb7"); var comicInfo = _archiveService.GetComicInfo(archive); @@ -314,7 +312,7 @@ public class ArchiveServiceTests [Fact] public void CanParseComicInfo() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/ComicInfos"); var archive = Path.Join(testDirectory, "ComicInfo.zip"); var comicInfo = _archiveService.GetComicInfo(archive); @@ -338,7 +336,7 @@ public class ArchiveServiceTests [Fact] public void CanParseComicInfo_DefaultNumberIsBlank() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/ComicInfos"); var archive = Path.Join(testDirectory, "ComicInfo2.zip"); var comicInfo = _archiveService.GetComicInfo(archive); diff --git a/API.Tests/Services/BackupServiceTests.cs b/Kavita.Services.Tests/BackupServiceTests.cs similarity index 89% rename from API.Tests/Services/BackupServiceTests.cs rename to Kavita.Services.Tests/BackupServiceTests.cs index 284bbc058..423b07fbb 100644 --- a/API.Tests/Services/BackupServiceTests.cs +++ b/Kavita.Services.Tests/BackupServiceTests.cs @@ -1,30 +1,19 @@ -using System; -using System.Data.Common; -using System.IO; +using System.Collections; using System.IO.Abstractions.TestingHelpers; -using System.Linq; using System.Reflection; -using System.Threading.Tasks; -using API.Data; -using API.Data.AutoMapper; -using API.Entities.Enums; -using API.Helpers; -using API.Helpers.Builders; -using API.Services; -using API.Services.Tasks; -using API.SignalR; using AutoMapper; using Hangfire; -using Microsoft.Data.Sqlite; +using Kavita.API.Services.SignalR; +using Kavita.Database; +using Kavita.Database.Tests; +using Kavita.Models.AutoMapper; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class BackupServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { @@ -47,8 +36,8 @@ public class BackupServiceTests(ITestOutputHelper outputHelper): AbstractDbTest( var backupService = new BackupService(_logger, unitOfWork, ds, _messageHub); var backupLogFiles = backupService.GetLogFiles(false).ToList(); - Assert.Single(backupLogFiles); - Assert.Equal(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath($"{LogDirectory}kavita.log"), API.Services.Tasks.Scanner.Parser.Parser.NormalizePath(backupLogFiles.First())); + Assert.Single((IEnumerable)backupLogFiles); + Assert.Equal(Parser.NormalizePath($"{LogDirectory}kavita.log"), Parser.NormalizePath(backupLogFiles.First())); } [Fact] @@ -63,8 +52,8 @@ public class BackupServiceTests(ITestOutputHelper outputHelper): AbstractDbTest( var ds = new DirectoryService(Substitute.For>(), filesystem); var backupService = new BackupService(_logger, unitOfWork, ds, _messageHub); - var backupLogFiles = backupService.GetLogFiles().Select(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath).ToList(); - Assert.Contains(backupLogFiles, file => file.Equals(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath($"{LogDirectory}kavita.log")) || file.Equals(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath($"{LogDirectory}kavita1.log"))); + var backupLogFiles = backupService.GetLogFiles().Select(Parser.NormalizePath).ToList(); + Assert.Contains(backupLogFiles, file => file.Equals(Parser.NormalizePath($"{LogDirectory}kavita.log")) || file.Equals(Parser.NormalizePath($"{LogDirectory}kavita1.log"))); } diff --git a/API.Tests/Services/BookServiceTests.cs b/Kavita.Services.Tests/BookServiceTests.cs similarity index 87% rename from API.Tests/Services/BookServiceTests.cs rename to Kavita.Services.Tests/BookServiceTests.cs index 1fb4c175d..edf94f639 100644 --- a/API.Tests/Services/BookServiceTests.cs +++ b/Kavita.Services.Tests/BookServiceTests.cs @@ -1,15 +1,12 @@ -using System.IO; -using System.IO.Abstractions; -using System.Threading.Tasks; -using API.Data; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using System.IO.Abstractions; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class BookServiceTests { @@ -32,14 +29,14 @@ public class BookServiceTests [InlineData("test.pdf", 1)] public void GetNumberOfPagesTest(string filePath, int expectedPages) { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/BookService"); Assert.Equal(expectedPages, _bookService.GetNumberOfPages(Path.Join(testDirectory, filePath))); } [Fact] public void ShouldHaveComicInfo() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/BookService"); var archive = Path.Join(testDirectory, "The Golden Harpoon; Or, Lost Among the Floes A Story of the Whaling Grounds.epub"); const string summaryInfo = "Book Description"; @@ -52,7 +49,7 @@ public class BookServiceTests [Fact] public void ShouldHaveComicInfo_WithAuthors() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/BookService"); var archive = Path.Join(testDirectory, "The Golden Harpoon; Or, Lost Among the Floes A Story of the Whaling Grounds.epub"); var comicInfo = _bookService.GetComicInfo(archive); @@ -63,7 +60,7 @@ public class BookServiceTests [Fact] public void ShouldParseAsVolumeGroup_WithoutSeriesIndex() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/BookService"); var archive = Path.Join(testDirectory, "TitleWithVolume_NoSeriesOrSeriesIndex.epub"); var comicInfo = _bookService.GetComicInfo(archive); @@ -75,7 +72,7 @@ public class BookServiceTests [Fact] public void ShouldParseAsVolumeGroup_WithSeriesIndex() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/BookService"); var archive = Path.Join(testDirectory, "TitleWithVolume.epub"); var comicInfo = _bookService.GetComicInfo(archive); @@ -87,7 +84,7 @@ public class BookServiceTests [Fact] public void ShouldHaveComicInfoForPdf() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/BookService"); var document = Path.Join(testDirectory, "test.pdf"); var comicInfo = _bookService.GetComicInfo(document); Assert.NotNull(comicInfo); @@ -98,7 +95,7 @@ public class BookServiceTests //[Fact] public void ShouldUsePdfInfoDict() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/Library/Books/PDFs"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ScannerService/Library/Books/PDFs"); var document = Path.Join(testDirectory, "Rollo at Work SP01.pdf"); var comicInfo = _bookService.GetComicInfo(document); Assert.NotNull(comicInfo); @@ -110,7 +107,7 @@ public class BookServiceTests [Fact] public void ShouldHandleIndirectPdfObjects() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/BookService"); var document = Path.Join(testDirectory, "indirect.pdf"); var comicInfo = _bookService.GetComicInfo(document); Assert.NotNull(comicInfo); @@ -121,7 +118,7 @@ public class BookServiceTests [Fact] public void FailGracefullyWithEncryptedPdf() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/BookService"); var document = Path.Join(testDirectory, "encrypted.pdf"); var comicInfo = _bookService.GetComicInfo(document); Assert.Null(comicInfo); @@ -133,7 +130,7 @@ public class BookServiceTests var ds = new DirectoryService(Substitute.For>(), new FileSystem()); var pdfParser = new PdfParser(ds); - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/BookService"); var filePath = Path.Join(testDirectory, "Bizet-Variations_Chromatiques_de_concert_Theme_A4.pdf"); var comicInfo = _bookService.GetComicInfo(filePath); @@ -151,7 +148,7 @@ public class BookServiceTests [Fact] public async Task ShouldBeAbleToLookUpImage() { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/BookService"); var filePath = Path.Join(testDirectory, "Relative Key Test File.epub"); var result = await _bookService.GetResourceAsync(filePath, "./images/titlepage800.png"); diff --git a/API.Tests/Services/BookmarkServiceTests.cs b/Kavita.Services.Tests/BookmarkServiceTests.cs similarity index 91% rename from API.Tests/Services/BookmarkServiceTests.cs rename to Kavita.Services.Tests/BookmarkServiceTests.cs index 0e486d844..669d25e7f 100644 --- a/API.Tests/Services/BookmarkServiceTests.cs +++ b/Kavita.Services.Tests/BookmarkServiceTests.cs @@ -1,21 +1,20 @@ -using System.Collections.Generic; -using System.IO; +using System.Collections; using System.IO.Abstractions.TestingHelpers; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Reader; -using API.Entities; -using API.Entities.Enums; -using API.Helpers.Builders; -using API.Services; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class BookmarkServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { @@ -38,7 +37,7 @@ Substitute.For()); var series = new SeriesBuilder("Test") .WithFormat(MangaFormat.Epub) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1") .Build()) .Build()) @@ -69,7 +68,7 @@ Substitute.For()); Assert.True(result); - Assert.Single(ds.GetFiles(BookmarkDirectory, searchOption:SearchOption.AllDirectories)); + Assert.Single((IEnumerable)ds.GetFiles(BookmarkDirectory, searchOption:SearchOption.AllDirectories)); Assert.NotNull(await unitOfWork.UserRepository.GetBookmarkAsync(1)); } @@ -86,7 +85,7 @@ Substitute.For()); .WithFormat(MangaFormat.Epub) .WithVolume(new VolumeBuilder("1") .WithMinNumber(1) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .Build()) .Build()) .Build(); @@ -262,7 +261,7 @@ Substitute.For()); var files = await bookmarkService.GetBookmarkFilesById(new[] {1}); var actualFiles = ds.GetFiles(BookmarkDirectory, searchOption: SearchOption.AllDirectories); - Assert.Equal(files.Select(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath).ToList(), actualFiles.Select(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath).ToList()); + Assert.Equal(files.Select(Parser.NormalizePath).ToList(), actualFiles.Select(Parser.NormalizePath).ToList()); } diff --git a/API.Tests/FakeHybridCache.cs b/Kavita.Services.Tests/Cache/FakeHybridCache.cs similarity index 96% rename from API.Tests/FakeHybridCache.cs rename to Kavita.Services.Tests/Cache/FakeHybridCache.cs index 65629a541..b6f2ada54 100644 --- a/API.Tests/FakeHybridCache.cs +++ b/Kavita.Services.Tests/Cache/FakeHybridCache.cs @@ -1,11 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Hybrid; +using Microsoft.Extensions.Caching.Hybrid; -namespace API.Tests; +namespace Kavita.Services.Tests.Cache; public class FakeHybridCache : HybridCache { diff --git a/API.Tests/FakeHybridCacheWithTracking.cs b/Kavita.Services.Tests/Cache/FakeHybridCacheWithTracking.cs similarity index 87% rename from API.Tests/FakeHybridCacheWithTracking.cs rename to Kavita.Services.Tests/Cache/FakeHybridCacheWithTracking.cs index 21c8dd691..d2ae17b90 100644 --- a/API.Tests/FakeHybridCacheWithTracking.cs +++ b/Kavita.Services.Tests/Cache/FakeHybridCacheWithTracking.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Hybrid; +using Microsoft.Extensions.Caching.Hybrid; -namespace API.Tests; +namespace Kavita.Services.Tests.Cache; public class FakeHybridCacheWithTracking : FakeHybridCache { diff --git a/API.Tests/Services/CacheServiceTests.cs b/Kavita.Services.Tests/CacheServiceTests.cs similarity index 97% rename from API.Tests/Services/CacheServiceTests.cs rename to Kavita.Services.Tests/CacheServiceTests.cs index a1464ba83..b09198f03 100644 --- a/API.Tests/Services/CacheServiceTests.cs +++ b/Kavita.Services.Tests/CacheServiceTests.cs @@ -1,18 +1,17 @@ -using System.IO; -using System.IO.Abstractions.TestingHelpers; -using System.Linq; -using System.Threading.Tasks; -using API.Data.Metadata; -using API.Entities.Enums; -using API.Helpers.Builders; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using System.IO.Abstractions.TestingHelpers; +using Kavita.API.Services; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Models.Parser; +using Kavita.Services.Builders; +using Kavita.Services.Reading; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; internal class MockReadingItemServiceForCacheService : IReadingItemService { diff --git a/API.Tests/Services/CleanupServiceTests.cs b/Kavita.Services.Tests/CleanupServiceTests.cs similarity index 95% rename from API.Tests/Services/CleanupServiceTests.cs rename to Kavita.Services.Tests/CleanupServiceTests.cs index 3b7cff2a6..6123f3cc4 100644 --- a/API.Tests/Services/CleanupServiceTests.cs +++ b/Kavita.Services.Tests/CleanupServiceTests.cs @@ -1,29 +1,29 @@ -using System; -using System.Collections.Generic; -using System.IO; +using System.Collections; using System.IO.Abstractions.TestingHelpers; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Filtering; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Progress; -using API.Extensions; -using API.Helpers; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; -using API.Services.Reading; -using API.Services.Tasks; -using API.SignalR; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Database; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; +using Kavita.Services.Reading; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class CleanupServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { @@ -136,7 +136,7 @@ public class CleanupServiceTests(ITestOutputHelper outputHelper): AbstractDbTest // Add 2 series with cover images context.Series.Add(new SeriesBuilder("Test 1") .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithCoverImage("v01_c01.jpg").Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithCoverImage("v01_c01.jpg").Build()) .WithCoverImage("v01_c01.jpg") .Build()) .WithCoverImage("series_01.jpg") @@ -145,7 +145,7 @@ public class CleanupServiceTests(ITestOutputHelper outputHelper): AbstractDbTest context.Series.Add(new SeriesBuilder("Test 2") .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithCoverImage("v01_c03.jpg").Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithCoverImage("v01_c03.jpg").Build()) .WithCoverImage("v01_c03.jpg") .Build()) .WithCoverImage("series_03.jpg") @@ -308,7 +308,7 @@ public class CleanupServiceTests(ITestOutputHelper outputHelper): AbstractDbTest var ds = new DirectoryService(Substitute.For>(), filesystem); var cleanupService = new CleanupService(logger, unitOfWork, messageHub, ds); await cleanupService.CleanupBackups(); - Assert.Single(ds.GetFiles(BackupDirectory, searchOption: SearchOption.AllDirectories)); + Assert.Single((IEnumerable)ds.GetFiles(BackupDirectory, searchOption: SearchOption.AllDirectories)); } [Fact] @@ -345,7 +345,7 @@ public class CleanupServiceTests(ITestOutputHelper outputHelper): AbstractDbTest var filesystem = CreateFileSystem(); foreach (var i in Enumerable.Range(1, 10)) { - var day = API.Services.Tasks.Scanner.Parser.Parser.PadZeros($"{i}"); + var day = Parser.PadZeros($"{i}"); filesystem.AddFile($"{LogDirectory}kavita202009{day}.log", new MockFileData("") { CreationTime = DateTimeOffset.Now.Subtract(TimeSpan.FromDays(31)) @@ -358,7 +358,7 @@ public class CleanupServiceTests(ITestOutputHelper outputHelper): AbstractDbTest var ds = new DirectoryService(Substitute.For>(), filesystem); var cleanupService = new CleanupService(logger, unitOfWork, messageHub, ds); await cleanupService.CleanupLogs(); - Assert.Single(ds.GetFiles(LogDirectory, searchOption: SearchOption.AllDirectories)); + Assert.Single((IEnumerable)ds.GetFiles(LogDirectory, searchOption: SearchOption.AllDirectories)); } [Fact] @@ -367,7 +367,7 @@ public class CleanupServiceTests(ITestOutputHelper outputHelper): AbstractDbTest var filesystem = CreateFileSystem(); foreach (var i in Enumerable.Range(1, 9)) { - var day = API.Services.Tasks.Scanner.Parser.Parser.PadZeros($"{i}"); + var day = Parser.PadZeros($"{i}"); filesystem.AddFile($"{LogDirectory}kavita202009{day}.log", new MockFileData("") { CreationTime = DateTimeOffset.Now.Subtract(TimeSpan.FromDays(31 - i)) @@ -402,12 +402,12 @@ public class CleanupServiceTests(ITestOutputHelper outputHelper): AbstractDbTest var (unitOfWork, context, _) = await CreateDatabase(); var (logger, messageHub, readerService) = await Setup(unitOfWork, context); - var c = new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + var c = new ChapterBuilder(Parser.DefaultChapter) .WithPages(1) .Build(); var series = new SeriesBuilder("Test") .WithFormat(MangaFormat.Epub) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(c) .Build()) .Build(); @@ -622,7 +622,7 @@ public class CleanupServiceTests(ITestOutputHelper outputHelper): AbstractDbTest c.UserProgress = new List(); s.Volumes = new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume).WithChapter(c).Build() + new VolumeBuilder(Parser.LooseLeafVolume).WithChapter(c).Build() }; context.Series.Add(s); diff --git a/API.Tests/Services/ClientDeviceServiceTests.cs b/Kavita.Services.Tests/ClientDeviceServiceTests.cs similarity index 85% rename from API.Tests/Services/ClientDeviceServiceTests.cs rename to Kavita.Services.Tests/ClientDeviceServiceTests.cs index 97783f8ca..13384eccb 100644 --- a/API.Tests/Services/ClientDeviceServiceTests.cs +++ b/Kavita.Services.Tests/ClientDeviceServiceTests.cs @@ -1,22 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Constants; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Progress; -using API.Entities.User; -using API.Helpers.Builders; -using API.Services; +using System.Collections; using Kavita.Common; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; #nullable enable public class ClientDeviceServiceTests : AbstractDbTest @@ -34,8 +28,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task IdentifyOrRegisterDeviceAsync_RegistersNewDevice_WhenNoExistingMatch() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -54,15 +48,15 @@ public class ClientDeviceServiceTests : AbstractDbTest Assert.Equal("Chrome on Windows", device.FriendlyName); Assert.True(device.IsActive); Assert.NotNull(device.CurrentClientInfo); - Assert.Single(device.History); + Assert.Single((IEnumerable)device.History); } [Fact] public async Task IdentifyOrRegisterDeviceAsync_MatchesExistingDevice_ByClientDeviceId() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -99,8 +93,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task IdentifyOrRegisterDeviceAsync_MatchesExistingDevice_ByFingerprint_WhenClientDeviceIdNull() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -127,8 +121,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task IdentifyOrRegisterDeviceAsync_UpdatesClientDeviceId_WhenFingerprintMatches() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -152,8 +146,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task IdentifyOrRegisterDeviceAsync_UsesFuzzyMatching_ForBrowserVersionUpgrade() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -174,8 +168,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task IdentifyOrRegisterDeviceAsync_CreatesNewDevice_WhenPlatformChanges() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -196,8 +190,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task IdentifyOrRegisterDeviceAsync_IgnoresInactiveDevices() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -236,8 +230,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task GenerateDeviceFingerprint_GeneratesConsistentHash_ForSameInput() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -259,8 +253,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task GenerateDeviceFingerprint_Fallbacks_WhenBrowserChangesOneMajorVersion() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -281,8 +275,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task GenerateDeviceFingerprint_GeneratesDifferentHash_WhenBrowserChangesTwoMajorVersions() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -302,8 +296,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task GenerateDeviceFingerprint_IsCaseInsensitive() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -325,8 +319,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task GenerateDeviceFingerprint_UsesMajorVersionOnly_ForFingerprinting() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -348,8 +342,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task IdentifyOrRegisterDeviceAsync_MatchesSameDevice_ForMinorBrowserVersionChanges() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -375,8 +369,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task FuzzyMatching_Matches_WithHighSimilarity() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -398,8 +392,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task FuzzyMatching_DoesNotMatch_WithLowSimilarity() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -421,8 +415,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task FuzzyMatching_OnlyConsidersRecentDevices_Within30Days() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -460,8 +454,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task UpdateDeviceActivity_UpdatesLastSeenUtc() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -484,8 +478,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task UpdateDeviceActivity_AddsHistoryRecord_WhenMeaningfulChanges() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -510,8 +504,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task UpdateDeviceActivity_DoesNotAddHistory_ForNonMeaningfulChanges() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -542,8 +536,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task GenerateFriendlyName_IncludesBrowserAndPlatform() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -562,8 +556,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task GenerateFriendlyName_UsesClientType_WhenNotWebBrowser() { // TODO: Remove these tests - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -582,8 +576,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task GenerateFriendlyName_HandlesNoPlatform() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -606,8 +600,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task GetUserDevicesAsync_ReturnsOnlyActiveDevices_ByDefault() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -619,7 +613,7 @@ public class ClientDeviceServiceTests : AbstractDbTest await context.SaveChangesAsync(); // Act - var devices = (await service.GetUserDevicesAsync(user.Id, includeInactive: false)).ToList(); + var devices = (await unitOfWork.ClientDeviceRepository.GetUserDevicesAsync(user.Id, includeInactive: false)).ToList(); // Assert Assert.Single(devices); @@ -630,8 +624,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task GetUserDevicesAsync_ReturnsAllDevices_WhenIncludeInactiveTrue() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -643,7 +637,7 @@ public class ClientDeviceServiceTests : AbstractDbTest await context.SaveChangesAsync(); // Act - var devices = await service.GetUserDevicesAsync(user.Id, includeInactive: true); + var devices = await unitOfWork.ClientDeviceRepository.GetUserDevicesAsync(user.Id, includeInactive: true); // Assert Assert.Equal(2, devices.Count()); @@ -653,8 +647,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task RenameDeviceAsync_UpdatesDeviceName() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -677,8 +671,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task RenameDeviceAsync_ReturnsFalse_WhenDeviceNotFound() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -695,8 +689,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task RemoveDeviceAsync_MarksDeviceAsInactive() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); @@ -719,8 +713,8 @@ public class ClientDeviceServiceTests : AbstractDbTest public async Task RemoveDeviceAsync_ReturnsFalse_WhenDeviceNotFound() { - var (_, context, mapper) = await CreateDatabase(); - var service = new ClientDeviceService(context, mapper, _logger); + var (unitOfWork, context, _) = await CreateDatabase(); + var service = new ClientDeviceService(context, unitOfWork, _logger); var user = new AppUserBuilder("testuser", "test@localhost").Build(); context.AppUser.Add(user); diff --git a/API.Tests/Services/CollectionTagServiceTests.cs b/Kavita.Services.Tests/CollectionTagServiceTests.cs similarity index 96% rename from API.Tests/Services/CollectionTagServiceTests.cs rename to Kavita.Services.Tests/CollectionTagServiceTests.cs index e6b0b5000..178ec9c10 100644 --- a/API.Tests/Services/CollectionTagServiceTests.cs +++ b/Kavita.Services.Tests/CollectionTagServiceTests.cs @@ -1,25 +1,21 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Collection; -using API.Entities; -using API.Entities.Enums; -using API.Extensions.QueryExtensions; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; -using API.SignalR; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; using Kavita.Common; -using Microsoft.EntityFrameworkCore; +using Kavita.Database; +using Kavita.Database.Extensions; +using Kavita.Database.Tests; +using Kavita.Models; +using Kavita.Models.Builders; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Collection; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class CollectionTagServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { @@ -29,7 +25,7 @@ public class CollectionTagServiceTests(ITestOutputHelper outputHelper): Abstract if (context.AppUserCollection.Any()) { - return new CollectionTagService(unitOfWork, Substitute.For()); + return new CollectionTagService(unitOfWork, Substitute.For(), Substitute.For()); } var s1 = new SeriesBuilder("Series 1").WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Mature).Build()).Build(); @@ -39,7 +35,7 @@ public class CollectionTagServiceTests(ITestOutputHelper outputHelper): Abstract .WithSeries(s2) .Build()); - var user = new AppUserBuilder("majora2007", "majora2007", Seed.DefaultThemes.First()).Build(); + var user = new AppUserBuilder("majora2007", "majora2007", Defaults.DefaultThemes.First()).Build(); user.Collections = new List() { new AppUserCollectionBuilder("Tag 1").WithItems(new []{s1}).Build(), @@ -49,7 +45,7 @@ public class CollectionTagServiceTests(ITestOutputHelper outputHelper): Abstract await unitOfWork.CommitAsync(); - return new CollectionTagService(unitOfWork, Substitute.For()); + return new CollectionTagService(unitOfWork, Substitute.For(), Substitute.For()); } #region DeleteTag @@ -199,7 +195,7 @@ public class CollectionTagServiceTests(ITestOutputHelper outputHelper): Abstract var service = await Setup(unitOfWork, context); // Create a second user - var user2 = new AppUserBuilder("user2", "user2", Seed.DefaultThemes.First()).Build(); + var user2 = new AppUserBuilder("user2", "user2", Defaults.DefaultThemes.First()).Build(); unitOfWork.UserRepository.Add(user2); await unitOfWork.CommitAsync(); diff --git a/Kavita.Services.Tests/Comparers/ChapterSortComparerTest.cs b/Kavita.Services.Tests/Comparers/ChapterSortComparerTest.cs new file mode 100644 index 000000000..4bea7d3a6 --- /dev/null +++ b/Kavita.Services.Tests/Comparers/ChapterSortComparerTest.cs @@ -0,0 +1,18 @@ +using Kavita.Services.Comparators; +using Kavita.Services.Scanner; + +namespace Kavita.Services.Tests.Comparers; + +public class ChapterSortComparerDefaultLastTest +{ + [Theory] + [InlineData(new[] {1, 2, Parser.DefaultChapterNumber}, new[] {1, 2, Parser.DefaultChapterNumber})] + [InlineData(new[] {3, 1, 2}, new[] {1, 2, 3})] + [InlineData(new[] {1, Parser.DefaultChapterNumber, Parser.DefaultChapterNumber}, new[] {1, Parser.DefaultChapterNumber, Parser.DefaultChapterNumber})] + [InlineData(new[] {Parser.DefaultChapterNumber, 1}, new[] {1, Parser.DefaultChapterNumber})] + public void ChapterSortTest(int[] input, int[] expected) + { + Assert.Equal(expected, input.OrderBy(f => f, new ChapterSortComparerDefaultLast()).ToArray()); + } + +} diff --git a/API.Tests/Comparers/ChapterSortComparerZeroFirstTests.cs b/Kavita.Services.Tests/Comparers/ChapterSortComparerZeroFirstTests.cs similarity index 88% rename from API.Tests/Comparers/ChapterSortComparerZeroFirstTests.cs rename to Kavita.Services.Tests/Comparers/ChapterSortComparerZeroFirstTests.cs index fbae46b59..ecacd4864 100644 --- a/API.Tests/Comparers/ChapterSortComparerZeroFirstTests.cs +++ b/Kavita.Services.Tests/Comparers/ChapterSortComparerZeroFirstTests.cs @@ -1,8 +1,6 @@ -using System.Linq; -using API.Comparators; -using Xunit; +using Kavita.Services.Comparators; -namespace API.Tests.Comparers; +namespace Kavita.Services.Tests.Comparers; public class ChapterSortComparerDefaultFirstTests { diff --git a/Kavita.Services.Tests/Comparers/SortComparerZeroLastTests.cs b/Kavita.Services.Tests/Comparers/SortComparerZeroLastTests.cs new file mode 100644 index 000000000..d432087f0 --- /dev/null +++ b/Kavita.Services.Tests/Comparers/SortComparerZeroLastTests.cs @@ -0,0 +1,16 @@ +using Kavita.Services.Comparators; +using Kavita.Services.Scanner; + +namespace Kavita.Services.Tests.Comparers; + +public class SortComparerZeroLastTests +{ + [Theory] + [InlineData(new[] {Parser.DefaultChapterNumber, 1, 2,}, new[] {1, 2, Parser.DefaultChapterNumber})] + [InlineData(new[] {3, 1, 2}, new[] {1, 2, 3})] + [InlineData(new[] {Parser.DefaultChapterNumber, Parser.DefaultChapterNumber, 1}, new[] {1, Parser.DefaultChapterNumber, Parser.DefaultChapterNumber})] + public void SortComparerZeroLastTest(int[] input, int[] expected) + { + Assert.Equal(expected, input.OrderBy(f => f, ChapterSortComparerDefaultLast.Default).ToArray()); + } +} diff --git a/API.Tests/Services/CoverDbServiceTests.cs b/Kavita.Services.Tests/CoverDbServiceTests.cs similarity index 90% rename from API.Tests/Services/CoverDbServiceTests.cs rename to Kavita.Services.Tests/CoverDbServiceTests.cs index d4886e0d9..2f7238df7 100644 --- a/API.Tests/Services/CoverDbServiceTests.cs +++ b/Kavita.Services.Tests/CoverDbServiceTests.cs @@ -1,34 +1,33 @@ -using System.IO; -using System.Reflection; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Entities.Enums; -using API.Extensions; -using API.Services; -using API.Services.Tasks.Metadata; -using API.SignalR; +using System.Reflection; using EasyCaching.Core; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Metadata; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Database.Tests; +using Kavita.Models.Constants; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Metadata; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class CoverDbServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { private static readonly IEasyCachingProviderFactory CacheFactory = Substitute.For(); private static readonly string FaviconPath = Path.Join(Directory.GetCurrentDirectory(), - "../../../Services/Test Data/CoverDbService/Favicons"); + "../../../Test Data/CoverDbService/Favicons"); /// /// Path to download files temp to. Should be empty after each test. /// private static readonly string TempPath = Path.Join(Directory.GetCurrentDirectory(), - "../../../Services/Test Data/CoverDbService/Temp"); + "../../../Test Data/CoverDbService/Temp"); private static Task<(IDirectoryService, ICoverDbService)> Setup(IUnitOfWork unitOfWork) diff --git a/API.Tests/Data/AesopsFables.epub b/Kavita.Services.Tests/Data/AesopsFables.epub similarity index 100% rename from API.Tests/Data/AesopsFables.epub rename to Kavita.Services.Tests/Data/AesopsFables.epub diff --git a/API.Tests/Services/DeviceServiceTests.cs b/Kavita.Services.Tests/DeviceServiceTests.cs similarity index 85% rename from API.Tests/Services/DeviceServiceTests.cs rename to Kavita.Services.Tests/DeviceServiceTests.cs index 12a79a03a..643610f4a 100644 --- a/API.Tests/Services/DeviceServiceTests.cs +++ b/Kavita.Services.Tests/DeviceServiceTests.cs @@ -1,17 +1,16 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using API.Data; -using API.DTOs.Device; -using API.DTOs.Device.EmailDevice; -using API.Entities; -using API.Entities.Enums.Device; -using API.Services; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Reading; +using Kavita.Database.Tests; +using Kavita.Models.DTOs.Device.EmailDevice; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums.Device; +using Kavita.Models.Entities.User; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class DeviceServiceDbTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { diff --git a/API.Tests/Services/DeviceTrackingServiceTests.cs b/Kavita.Services.Tests/DeviceTrackingServiceTests.cs similarity index 98% rename from API.Tests/Services/DeviceTrackingServiceTests.cs rename to Kavita.Services.Tests/DeviceTrackingServiceTests.cs index f62f5d474..2e8f065a7 100644 --- a/API.Tests/Services/DeviceTrackingServiceTests.cs +++ b/Kavita.Services.Tests/DeviceTrackingServiceTests.cs @@ -1,21 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using API.Entities.Enums; -using API.Entities.Progress; -using API.Entities.User; -using API.Helpers.Builders; -using API.Services; +using Kavita.API.Services; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; +using Kavita.Services.Tests.Cache; using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -using System.Linq; -using API.Entities; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; #nullable enable diff --git a/API.Tests/Services/DirectoryServiceTests.cs b/Kavita.Services.Tests/DirectoryServiceTests.cs similarity index 95% rename from API.Tests/Services/DirectoryServiceTests.cs rename to Kavita.Services.Tests/DirectoryServiceTests.cs index abc8254de..7839b9326 100644 --- a/API.Tests/Services/DirectoryServiceTests.cs +++ b/Kavita.Services.Tests/DirectoryServiceTests.cs @@ -1,20 +1,16 @@ -using System; -using System.Collections.Generic; +using System.Collections; using System.Globalization; -using System.IO; using System.IO.Abstractions.TestingHelpers; -using System.Linq; using System.Runtime.InteropServices; using System.Text; -using System.Threading.Tasks; -using API.Services; using Kavita.Common.Helpers; +using Kavita.Database.Tests; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class DirectoryServiceTests: AbstractFsTest { @@ -43,7 +39,7 @@ public class DirectoryServiceTests: AbstractFsTest var ds = new DirectoryService(Substitute.For>(), fileSystem); var files = new List(); var fileCount = ds.TraverseTreeParallelForEach(testDirectory, s => files.Add(s), - API.Services.Tasks.Scanner.Parser.Parser.ArchiveFileExtensions, _logger); + Parser.ArchiveFileExtensions, _logger); Assert.Equal(28, fileCount); Assert.Equal(28, files.Count); @@ -68,7 +64,7 @@ public class DirectoryServiceTests: AbstractFsTest try { var fileCount = ds.TraverseTreeParallelForEach("/manga/", s => files.Add(s), - API.Services.Tasks.Scanner.Parser.Parser.ImageFileExtensions, _logger); + Parser.ImageFileExtensions, _logger); Assert.Equal(1, fileCount); } catch @@ -100,7 +96,7 @@ public class DirectoryServiceTests: AbstractFsTest var ds = new DirectoryService(Substitute.For>(), fileSystem); var files = new List(); var fileCount = ds.TraverseTreeParallelForEach(testDirectory, s => files.Add(s), - API.Services.Tasks.Scanner.Parser.Parser.ArchiveFileExtensions, _logger); + Parser.ArchiveFileExtensions, _logger); Assert.Equal(28, fileCount); Assert.Equal(28, files.Count); @@ -121,7 +117,7 @@ public class DirectoryServiceTests: AbstractFsTest fileSystem.AddFile($"{testDirectory}file_{29}.jpg", new MockFileData("")); var ds = new DirectoryService(Substitute.For>(), fileSystem); - var files = ds.GetFilesWithExtension(testDirectory, API.Services.Tasks.Scanner.Parser.Parser.ArchiveFileExtensions); + var files = ds.GetFilesWithExtension(testDirectory, Parser.ArchiveFileExtensions); Assert.Equal(10, files.Length); Assert.All(files, s => fileSystem.Path.GetExtension(s).Equals(".zip")); @@ -160,7 +156,7 @@ public class DirectoryServiceTests: AbstractFsTest fileSystem.AddFile($"{testDirectory}file_{29}.jpg", new MockFileData("")); var ds = new DirectoryService(Substitute.For>(), fileSystem); - var files = ds.GetFiles(testDirectory, API.Services.Tasks.Scanner.Parser.Parser.ArchiveFileExtensions).ToList(); + var files = ds.GetFiles(testDirectory, Parser.ArchiveFileExtensions).ToList(); Assert.Equal(10, files.Count); Assert.All(files, s => fileSystem.Path.GetExtension(s).Equals(".zip")); @@ -403,7 +399,8 @@ public class DirectoryServiceTests: AbstractFsTest fileSystem.AddFile($"{testDirectory}data-0.txt", new MockFileData("abc")); var ds = new DirectoryService(Substitute.For>(), fileSystem); - Assert.False(ds.IsDriveMounted("d:/manga/")); + // Windows GA runners mount on D drive, so we use E to ensure this test passes + Assert.False(ds.IsDriveMounted("e:/manga/")); } [Fact] @@ -615,12 +612,12 @@ public class DirectoryServiceTests: AbstractFsTest var ds = new DirectoryService(Substitute.For>(), fileSystem); ds.CopyFilesToDirectory(new []{MockUnixSupport.Path($"{testDirectory}file.zip")}, "/manga/output/"); ds.CopyFilesToDirectory(new []{MockUnixSupport.Path($"{testDirectory}file.zip")}, "/manga/output/"); - var outputFiles = ds.GetFiles("/manga/output/").Select(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath).ToList(); + var outputFiles = ds.GetFiles("/manga/output/").Select(Parser.NormalizePath).ToList(); Assert.Equal(4, outputFiles.Count); // we have 2 already there and 2 copies // For some reason, this has C:/ on directory even though everything is emulated (System.IO.Abstractions issue, not changing) // https://github.com/TestableIO/System.IO.Abstractions/issues/831 - Assert.True(outputFiles.Contains(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath("/manga/output/file (3).zip")) - || outputFiles.Contains(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath("C:/manga/output/file (3).zip"))); + Assert.True(outputFiles.Contains(Parser.NormalizePath("/manga/output/file (3).zip")) + || outputFiles.Contains(Parser.NormalizePath("C:/manga/output/file (3).zip"))); } [Fact] @@ -632,12 +629,12 @@ public class DirectoryServiceTests: AbstractFsTest var ds = new DirectoryService(Substitute.For>(), fileSystem); ds.CopyFilesToDirectory(new []{MockUnixSupport.Path($"{testDirectory}file.zip")}, "/manga/output/", new [] {"01"}); - var outputFiles = ds.GetFiles("/manga/output/").Select(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath).ToList(); - Assert.Single(outputFiles); + var outputFiles = ds.GetFiles("/manga/output/").Select(Parser.NormalizePath).ToList(); + Assert.Single((IEnumerable)outputFiles); // For some reason, this has C:/ on directory even though everything is emulated (System.IO.Abstractions issue, not changing) // https://github.com/TestableIO/System.IO.Abstractions/issues/831 - Assert.True(outputFiles.Contains(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath("/manga/output/01.zip")) - || outputFiles.Contains(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath("C:/manga/output/01.zip"))); + Assert.True(outputFiles.Contains(Parser.NormalizePath("/manga/output/01.zip")) + || outputFiles.Contains(Parser.NormalizePath("C:/manga/output/01.zip"))); } #endregion @@ -683,7 +680,7 @@ public class DirectoryServiceTests: AbstractFsTest fileSystem.AddFile($"{testDirectory}file_0.zip", new MockFileData("")); var ds = new DirectoryService(Substitute.For>(), fileSystem); - Assert.Single(ds.ListDirectory(testDirectory)); + Assert.Single((IEnumerable)ds.ListDirectory(testDirectory)); } #endregion @@ -965,7 +962,7 @@ public class DirectoryServiceTests: AbstractFsTest var globMatcher = new GlobMatcher(); globMatcher.AddExclude("*.*"); - var allFiles = ds.ScanFiles("C:/Data/", API.Services.Tasks.Scanner.Parser.Parser.SupportedExtensions, globMatcher); + var allFiles = ds.ScanFiles("C:/Data/", Parser.SupportedExtensions, globMatcher); Assert.Empty(allFiles); @@ -991,9 +988,9 @@ public class DirectoryServiceTests: AbstractFsTest var globMatcher = new GlobMatcher(); globMatcher.AddExclude("**/Accel World/*"); - var allFiles = ds.ScanFiles("C:/Data/", API.Services.Tasks.Scanner.Parser.Parser.SupportedExtensions, globMatcher); + var allFiles = ds.ScanFiles("C:/Data/", Parser.SupportedExtensions, globMatcher); - Assert.Single(allFiles); // Ignore files are not counted in files, only valid extensions + Assert.Single((IEnumerable)allFiles); // Ignore files are not counted in files, only valid extensions return Task.CompletedTask; } @@ -1023,7 +1020,7 @@ public class DirectoryServiceTests: AbstractFsTest var globMatcher = new GlobMatcher(); globMatcher.AddExclude("**/Accel World/*"); globMatcher.AddExclude("**/ArtBooks/*"); - var allFiles = ds.ScanFiles("C:/Data/", API.Services.Tasks.Scanner.Parser.Parser.SupportedExtensions, globMatcher); + var allFiles = ds.ScanFiles("C:/Data/", Parser.SupportedExtensions, globMatcher); Assert.Equal(2, allFiles.Count); // Ignore files are not counted in files, only valid extensions @@ -1047,7 +1044,7 @@ public class DirectoryServiceTests: AbstractFsTest var ds = new DirectoryService(Substitute.For>(), fileSystem); - var allFiles = ds.ScanFiles("C:/Data/", API.Services.Tasks.Scanner.Parser.Parser.SupportedExtensions); + var allFiles = ds.ScanFiles("C:/Data/", Parser.SupportedExtensions); Assert.Equal(5, allFiles.Count); diff --git a/API.Tests/Entities/ComicInfoTests.cs b/Kavita.Services.Tests/Entities/ComicInfoTests.cs similarity index 90% rename from API.Tests/Entities/ComicInfoTests.cs rename to Kavita.Services.Tests/Entities/ComicInfoTests.cs index e43f4ee77..a832166a8 100644 --- a/API.Tests/Entities/ComicInfoTests.cs +++ b/Kavita.Services.Tests/Entities/ComicInfoTests.cs @@ -1,8 +1,8 @@ -using API.Data.Metadata; -using API.Entities.Enums; -using Xunit; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Services.Extensions; -namespace API.Tests.Entities; +namespace Kavita.Services.Tests.Entities; public class ComicInfoTests { @@ -103,7 +103,7 @@ public class ComicInfoTests public void IsValid(string code) { // Note: ASIN's starting with "B0" are not able to be converted to ISBN - Assert.Equal(code, ComicInfo.ParseGtin(code)); + Assert.Equal(code, ComicInfoExtensions.ParseGtin(code)); } [Theory] @@ -111,7 +111,7 @@ public class ComicInfoTests [InlineData("9504000059437 ")] public void IsInvalid(string code) { - Assert.Equal(string.Empty, ComicInfo.ParseGtin(code)); + Assert.Equal(string.Empty, ComicInfoExtensions.ParseGtin(code)); } #endregion } diff --git a/API.Tests/Services/EntityNamingServiceTests.cs b/Kavita.Services.Tests/EntityNamingServiceTests.cs similarity index 99% rename from API.Tests/Services/EntityNamingServiceTests.cs rename to Kavita.Services.Tests/EntityNamingServiceTests.cs index c6b6cac95..a1d5a1146 100644 --- a/API.Tests/Services/EntityNamingServiceTests.cs +++ b/Kavita.Services.Tests/EntityNamingServiceTests.cs @@ -1,13 +1,9 @@ -using System; -using System.Collections.Generic; -using API.DTOs; -using API.DTOs.ReadingLists; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner.Parser; -using Xunit; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Scanner; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; #nullable enable public class EntityNamingServiceTests diff --git a/API.Tests/Extensions/ChapterListExtensionsTests.cs b/Kavita.Services.Tests/Extensions/ChapterListExtensionsTests.cs similarity index 67% rename from API.Tests/Extensions/ChapterListExtensionsTests.cs rename to Kavita.Services.Tests/Extensions/ChapterListExtensionsTests.cs index 7ef546e4f..14c881010 100644 --- a/API.Tests/Extensions/ChapterListExtensionsTests.cs +++ b/Kavita.Services.Tests/Extensions/ChapterListExtensionsTests.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers.Builders; -using API.Services.Tasks.Scanner.Parser; -using Xunit; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Parser; +using Kavita.Services.Builders; +using Kavita.Services.Extensions; +using Kavita.Services.Scanner; -namespace API.Tests.Extensions; +namespace Kavita.Services.Tests.Extensions; public class ChapterListExtensionsTests { @@ -29,7 +27,7 @@ public class ChapterListExtensionsTests { var info = new ParserInfo() { - Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, + Chapters = Parser.DefaultChapter, Edition = "", Format = MangaFormat.Archive, FullFilePath = "/manga/darker than black.cbz", @@ -37,12 +35,12 @@ public class ChapterListExtensionsTests IsSpecial = false, Series = "darker than black", Title = "darker than black", - Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume + Volumes = Parser.LooseLeafVolume }; var chapterList = new List() { - CreateChapter("darker than black - Some special", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/darker than black - special.cbz", MangaFormat.Archive), true) + CreateChapter("darker than black - Some special", Parser.DefaultChapter, CreateFile("/manga/darker than black - special.cbz", MangaFormat.Archive), true) }; var actualChapter = chapterList.GetChapterByRange(info); @@ -56,7 +54,7 @@ public class ChapterListExtensionsTests { var info = new ParserInfo() { - Chapters = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, + Chapters = Parser.LooseLeafVolume, Edition = "", Format = MangaFormat.Archive, FullFilePath = "/manga/darker than black.cbz", @@ -64,12 +62,12 @@ public class ChapterListExtensionsTests IsSpecial = true, Series = "darker than black", Title = "darker than black", - Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume + Volumes = Parser.LooseLeafVolume }; var chapterList = new List() { - CreateChapter("darker than black", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/darker than black.cbz", MangaFormat.Archive), true) + CreateChapter("darker than black", Parser.DefaultChapter, CreateFile("/manga/darker than black.cbz", MangaFormat.Archive), true) }; var actualChapter = chapterList.GetChapterByRange(info); @@ -82,7 +80,7 @@ public class ChapterListExtensionsTests { var info = new ParserInfo() { - Chapters = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, + Chapters = Parser.LooseLeafVolume, Edition = "", Format = MangaFormat.Archive, FullFilePath = "/manga/detective comics #001.cbz", @@ -90,13 +88,13 @@ public class ChapterListExtensionsTests IsSpecial = true, Series = "detective comics", Title = "detective comics", - Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume + Volumes = Parser.LooseLeafVolume }; var chapterList = new List() { - CreateChapter("detective comics", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true), - CreateChapter("detective comics", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true) + CreateChapter("detective comics", Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true), + CreateChapter("detective comics", Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true) }; var actualChapter = chapterList.GetChapterByRange(info); @@ -117,7 +115,7 @@ public class ChapterListExtensionsTests IsSpecial = false, Series = "detective comics", Title = "detective comics", - Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume + Volumes = Parser.LooseLeafVolume }; var chapterList = new List() @@ -137,7 +135,7 @@ public class ChapterListExtensionsTests { var chapterList = new List() { - CreateChapter("darker than black", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/darker than black.cbz", MangaFormat.Archive), true), + CreateChapter("darker than black", Parser.DefaultChapter, CreateFile("/manga/darker than black.cbz", MangaFormat.Archive), true), CreateChapter("darker than black", "1", CreateFile("/manga/darker than black.cbz", MangaFormat.Archive), false), }; @@ -176,8 +174,8 @@ public class ChapterListExtensionsTests { var chapterList = new List() { - CreateChapter("detective comics", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true), - CreateChapter("detective comics", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true) + CreateChapter("detective comics", Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true), + CreateChapter("detective comics", Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true) }; chapterList[0].ReleaseDate = new DateTime(10, 1, 1, 0, 0, 0, DateTimeKind.Utc); @@ -191,8 +189,8 @@ public class ChapterListExtensionsTests { var chapterList = new List() { - CreateChapter("detective comics", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true), - CreateChapter("detective comics", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true) + CreateChapter("detective comics", Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true), + CreateChapter("detective comics", Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true) }; chapterList[0].ReleaseDate = new DateTime(2002, 1, 1, 0, 0, 0, DateTimeKind.Utc); diff --git a/API.Tests/Extensions/FilterDtoExtensionsTests.cs b/Kavita.Services.Tests/Extensions/FilterDtoExtensionsTests.cs similarity index 83% rename from API.Tests/Extensions/FilterDtoExtensionsTests.cs rename to Kavita.Services.Tests/Extensions/FilterDtoExtensionsTests.cs index c9985f509..bfb558269 100644 --- a/API.Tests/Extensions/FilterDtoExtensionsTests.cs +++ b/Kavita.Services.Tests/Extensions/FilterDtoExtensionsTests.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Generic; -using API.DTOs.Filtering; -using API.Entities.Enums; -using API.Extensions; -using Xunit; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Extensions; -namespace API.Tests.Extensions; +namespace Kavita.Services.Tests.Extensions; public class FilterDtoExtensionsTests { diff --git a/API.Tests/Extensions/ParserInfoListExtensionsTests.cs b/Kavita.Services.Tests/Extensions/ParserInfoListExtensionsTests.cs similarity index 86% rename from API.Tests/Extensions/ParserInfoListExtensionsTests.cs rename to Kavita.Services.Tests/Extensions/ParserInfoListExtensionsTests.cs index 227dd2b32..04511182a 100644 --- a/API.Tests/Extensions/ParserInfoListExtensionsTests.cs +++ b/Kavita.Services.Tests/Extensions/ParserInfoListExtensionsTests.cs @@ -1,17 +1,13 @@ -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions.TestingHelpers; -using System.Linq; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers.Builders; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using System.IO.Abstractions.TestingHelpers; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Parser; +using Kavita.Services.Builders; +using Kavita.Services.Extensions; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; -namespace API.Tests.Extensions; +namespace Kavita.Services.Tests.Extensions; public class ParserInfoListExtensions { @@ -41,7 +37,7 @@ public class ParserInfoListExtensions { infos.Add(_defaultParser.Parse( Path.Join("E:/Manga/Cynthia the Mission/", filename), - "E:/Manga/", "E:/Manga/", LibraryType.Manga)); + "E:/Manga/", "E:/Manga/", LibraryType.Manga)!); } var files = inputChapters.Select(s => new MangaFileBuilder(s, MangaFormat.Archive, 199).Build()).ToList(); @@ -59,7 +55,7 @@ public class ParserInfoListExtensions { _defaultParser.Parse( "E:/Manga/Cynthia the Mission/Cynthia The Mission The Special SP01 [Desudesu&Brolen].zip", - "E:/Manga/", "E:/Manga/", LibraryType.Manga) + "E:/Manga/", "E:/Manga/", LibraryType.Manga)! }; var files = new[] {@"E:\Manga\Cynthia the Mission\Cynthia The Mission The Special SP01 [Desudesu&Brolen].zip"} diff --git a/API.Tests/Extensions/SeriesExtensionsTests.cs b/Kavita.Services.Tests/Extensions/SeriesExtensionsTests.cs similarity index 98% rename from API.Tests/Extensions/SeriesExtensionsTests.cs rename to Kavita.Services.Tests/Extensions/SeriesExtensionsTests.cs index adaecfba5..d9c0ece19 100644 --- a/API.Tests/Extensions/SeriesExtensionsTests.cs +++ b/Kavita.Services.Tests/Extensions/SeriesExtensionsTests.cs @@ -1,12 +1,11 @@ -using System.Linq; -using API.Comparators; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers.Builders; -using API.Services.Tasks.Scanner.Parser; -using Xunit; +using Kavita.Models.Builders; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Builders; +using Kavita.Services.Comparators; +using Kavita.Services.Extensions; +using Kavita.Services.Scanner; -namespace API.Tests.Extensions; +namespace Kavita.Services.Tests.Extensions; public class SeriesExtensionsTests { diff --git a/API.Tests/Extensions/SeriesFilterTests.cs b/Kavita.Services.Tests/Extensions/SeriesFilterTests.cs similarity index 98% rename from API.Tests/Extensions/SeriesFilterTests.cs rename to Kavita.Services.Tests/Extensions/SeriesFilterTests.cs index dedc43b05..64cdf1b6b 100644 --- a/API.Tests/Extensions/SeriesFilterTests.cs +++ b/Kavita.Services.Tests/Extensions/SeriesFilterTests.cs @@ -1,27 +1,26 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.DTOs; -using API.DTOs.Filtering.v2; -using API.DTOs.Progress; -using API.Entities; -using API.Entities.Enums; -using API.Extensions.QueryExtensions.Filtering; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; -using API.Services.Reading; -using API.SignalR; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Database; +using Kavita.Database.Extensions.Filters; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; +using Kavita.Services.Reading; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Extensions; +namespace Kavita.Services.Tests.Extensions; public class SeriesFilterTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { diff --git a/API.Tests/Extensions/VolumeListExtensionsTests.cs b/Kavita.Services.Tests/Extensions/VolumeListExtensionsTests.cs similarity index 59% rename from API.Tests/Extensions/VolumeListExtensionsTests.cs rename to Kavita.Services.Tests/Extensions/VolumeListExtensionsTests.cs index bbb8f215c..583454a4a 100644 --- a/API.Tests/Extensions/VolumeListExtensionsTests.cs +++ b/Kavita.Services.Tests/Extensions/VolumeListExtensionsTests.cs @@ -1,11 +1,10 @@ -using System.Collections.Generic; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers.Builders; -using Xunit; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Builders; +using Kavita.Services.Extensions; +using Kavita.Services.Scanner; -namespace API.Tests.Extensions; +namespace Kavita.Services.Tests.Extensions; public class VolumeListExtensionsTests { @@ -20,14 +19,14 @@ public class VolumeListExtensionsTests .WithChapter(new ChapterBuilder("3").Build()) .WithChapter(new ChapterBuilder("4").Build()) .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").Build()) .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .Build()) .Build(), }; @@ -42,16 +41,16 @@ public class VolumeListExtensionsTests var volumes = new List() { new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).Build()) .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("0.5").Build()) .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .Build()) .Build(), }; @@ -69,13 +68,13 @@ public class VolumeListExtensionsTests .WithChapter(new ChapterBuilder("3").Build()) .WithChapter(new ChapterBuilder("4").Build()) .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").Build()) .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .Build()) .Build(), }; @@ -92,13 +91,13 @@ public class VolumeListExtensionsTests .WithChapter(new ChapterBuilder("3").Build()) .WithChapter(new ChapterBuilder("4").Build()) .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").Build()) .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .Build()) .Build(), }; @@ -115,13 +114,13 @@ public class VolumeListExtensionsTests .WithChapter(new ChapterBuilder("3").Build()) .WithChapter(new ChapterBuilder("4").Build()) .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").Build()) .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .Build()) .Build(), }; @@ -141,10 +140,10 @@ public class VolumeListExtensionsTests new VolumeBuilder("1") .WithChapter(new ChapterBuilder("1").Build()) .Build(), - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .Build()) .Build(), }; diff --git a/API.Tests/Services/ExternalMetadataServiceTests.cs b/Kavita.Services.Tests/ExternalMetadataServiceTests.cs similarity index 99% rename from API.Tests/Services/ExternalMetadataServiceTests.cs rename to Kavita.Services.Tests/ExternalMetadataServiceTests.cs index 0a94b3d36..60f0e538d 100644 --- a/API.Tests/Services/ExternalMetadataServiceTests.cs +++ b/Kavita.Services.Tests/ExternalMetadataServiceTests.cs @@ -1,32 +1,31 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Recommendation; -using API.DTOs.Scrobbling; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Entities.MetadataMatching; -using API.Entities.Person; -using API.Helpers.Builders; -using API.Services.Plus; -using API.Services.Tasks.Metadata; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; -using AutoMapper; +using AutoMapper; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services.Metadata; +using Kavita.API.Services.Plus; +using Kavita.API.Services.SignalR; +using Kavita.Database; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Recommendation; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.MetadataMatching; +using Kavita.Models.Entities.Person; +using Kavita.Services.Builders; +using Kavita.Services.Plus; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; /// /// Given these rely on Kavita+, this will not have any [Fact]/[Theory] on them and must be manually checked @@ -101,6 +100,9 @@ public class ExternalMetadataServiceTests: AbstractDbTest Substitute.For(), Substitute.For(), Substitute.For()); + // Clear tracker so test body starts with a clean slate + context.ChangeTracker.Clear(); + return (externalMetadataService, genreLookup, tagLookup, personLookup); } diff --git a/API.Tests/Services/FileSystemTests.cs b/Kavita.Services.Tests/FileSystemTests.cs similarity index 87% rename from API.Tests/Services/FileSystemTests.cs rename to Kavita.Services.Tests/FileSystemTests.cs index 97250ea45..6b306059d 100644 --- a/API.Tests/Services/FileSystemTests.cs +++ b/Kavita.Services.Tests/FileSystemTests.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; -using System.IO.Abstractions.TestingHelpers; -using API.Services; -using Xunit; +using System.IO.Abstractions.TestingHelpers; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class FileSystemTests { diff --git a/API.Tests/Helpers/BookSortTitlePrefixHelperTests.cs b/Kavita.Services.Tests/Helpers/BookSortTitlePrefixHelperTests.cs similarity index 99% rename from API.Tests/Helpers/BookSortTitlePrefixHelperTests.cs rename to Kavita.Services.Tests/Helpers/BookSortTitlePrefixHelperTests.cs index e1f585806..671a32951 100644 --- a/API.Tests/Helpers/BookSortTitlePrefixHelperTests.cs +++ b/Kavita.Services.Tests/Helpers/BookSortTitlePrefixHelperTests.cs @@ -1,7 +1,6 @@ -using API.Helpers; -using Xunit; +using Kavita.Services.Helpers; -namespace API.Tests.Helpers; +namespace Kavita.Services.Tests.Helpers; public class BookSortTitlePrefixHelperTests { diff --git a/API.Tests/Helpers/CacheHelperTests.cs b/Kavita.Services.Tests/Helpers/CacheHelperTests.cs similarity index 97% rename from API.Tests/Helpers/CacheHelperTests.cs rename to Kavita.Services.Tests/Helpers/CacheHelperTests.cs index 3962ba2df..1afa25b92 100644 --- a/API.Tests/Helpers/CacheHelperTests.cs +++ b/Kavita.Services.Tests/Helpers/CacheHelperTests.cs @@ -1,14 +1,11 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions.TestingHelpers; -using API.Entities.Enums; -using API.Helpers; -using API.Helpers.Builders; -using API.Services; -using Xunit; +using System.IO.Abstractions.TestingHelpers; +using Kavita.API.Services.Helpers; +using Kavita.Database.Tests; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Builders; +using Kavita.Services.Helpers; -namespace API.Tests.Helpers; +namespace Kavita.Services.Tests.Helpers; public class CacheHelperTests: AbstractFsTest { diff --git a/API.Tests/Helpers/KoreaderHelperTests.cs b/Kavita.Services.Tests/Helpers/KoreaderHelperTests.cs similarity index 98% rename from API.Tests/Helpers/KoreaderHelperTests.cs rename to Kavita.Services.Tests/Helpers/KoreaderHelperTests.cs index 1d4710f2c..31ef27a56 100644 --- a/API.Tests/Helpers/KoreaderHelperTests.cs +++ b/Kavita.Services.Tests/Helpers/KoreaderHelperTests.cs @@ -1,9 +1,7 @@ -using API.DTOs.Progress; -using API.Helpers; -using Xunit; +using Kavita.Models.DTOs.Progress; +using Kavita.Services.Helpers; -namespace API.Tests.Helpers; -#nullable enable +namespace Kavita.Services.Tests.Helpers; public class KoreaderHelperTests { diff --git a/API.Tests/Helpers/OrderableHelperTests.cs b/Kavita.Services.Tests/Helpers/OrderableHelperTests.cs similarity index 97% rename from API.Tests/Helpers/OrderableHelperTests.cs rename to Kavita.Services.Tests/Helpers/OrderableHelperTests.cs index e0f18a60d..5cbeb730e 100644 --- a/API.Tests/Helpers/OrderableHelperTests.cs +++ b/Kavita.Services.Tests/Helpers/OrderableHelperTests.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using API.Entities; -using API.Helpers; -using Xunit; +using Kavita.Models.Entities; +using Kavita.Models.Entities.User; +using Kavita.Models.Helpers; -namespace API.Tests.Helpers; +namespace Kavita.Services.Tests.Helpers; public class OrderableHelperTests { diff --git a/API.Tests/Helpers/ParserInfoFactory.cs b/Kavita.Services.Tests/Helpers/ParserInfoFactory.cs similarity index 90% rename from API.Tests/Helpers/ParserInfoFactory.cs rename to Kavita.Services.Tests/Helpers/ParserInfoFactory.cs index 40d0ea4f4..5c1353974 100644 --- a/API.Tests/Helpers/ParserInfoFactory.cs +++ b/Kavita.Services.Tests/Helpers/ParserInfoFactory.cs @@ -1,13 +1,9 @@ using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using API.Entities.Enums; -using API.Extensions; -using API.Services.Tasks.Scanner; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Common.Extensions; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Parser; -namespace API.Tests.Helpers; +namespace Kavita.Services.Tests.Helpers; public static class ParserInfoFactory { diff --git a/API.Tests/Helpers/PersonHelperTests.cs b/Kavita.Services.Tests/Helpers/PersonHelperTests.cs similarity index 97% rename from API.Tests/Helpers/PersonHelperTests.cs rename to Kavita.Services.Tests/Helpers/PersonHelperTests.cs index cee7c47c8..333c7db5c 100644 --- a/API.Tests/Helpers/PersonHelperTests.cs +++ b/Kavita.Services.Tests/Helpers/PersonHelperTests.cs @@ -1,13 +1,11 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Entities.Enums; -using API.Helpers; -using API.Helpers.Builders; -using Xunit; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Builders; +using Kavita.Services.Helpers; using Xunit.Abstractions; -namespace API.Tests.Helpers; +namespace Kavita.Services.Tests.Helpers; public class PersonHelperTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { diff --git a/Kavita.Services.Tests/Helpers/ReviewHelperTests.cs b/Kavita.Services.Tests/Helpers/ReviewHelperTests.cs new file mode 100644 index 000000000..61764aa84 --- /dev/null +++ b/Kavita.Services.Tests/Helpers/ReviewHelperTests.cs @@ -0,0 +1,127 @@ +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Services.Helpers; + +namespace Kavita.Services.Tests.Helpers; + +public class ReviewHelperTests +{ + #region SelectSpectrumOfReviews Tests + + [Fact] + public void SelectSpectrumOfReviews_WhenLessThan10Reviews_ReturnsAllReviews() + { + + var reviews = CreateReviewList(8); + + // Act + var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); + + // Assert + Assert.Equal(8, result.Count); + Assert.Equal(reviews, result.OrderByDescending(r => r.Score)); + } + + [Fact] + public void SelectSpectrumOfReviews_WhenMoreThan10Reviews_Returns10Reviews() + { + + var reviews = CreateReviewList(20); + + // Act + var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); + + // Assert + Assert.Equal(10, result.Count); + Assert.Equal(reviews[0], result.First()); + Assert.Equal(reviews[19], result.Last()); + } + + [Fact] + public void SelectSpectrumOfReviews_WithExactly10Reviews_ReturnsAllReviews() + { + + var reviews = CreateReviewList(10); + + // Act + var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); + + // Assert + Assert.Equal(10, result.Count); + } + + [Fact] + public void SelectSpectrumOfReviews_WithLargeNumberOfReviews_ReturnsCorrectSpectrum() + { + + var reviews = CreateReviewList(100); + + // Act + var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); + + // Assert + Assert.Equal(10, result.Count); + Assert.Contains(reviews[0], result); + Assert.Contains(reviews[1], result); + Assert.Contains(reviews[98], result); + Assert.Contains(reviews[99], result); + } + + [Fact] + public void SelectSpectrumOfReviews_WithEmptyList_ReturnsEmptyList() + { + + var reviews = new List(); + + // Act + var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); + + // Assert + Assert.Empty(result); + } + + [Fact] + public void SelectSpectrumOfReviews_ResultsOrderedByScoreDescending() + { + + var reviews = new List + { + new UserReviewDto { Tagline = "1", Score = 3 }, + new UserReviewDto { Tagline = "2", Score = 5 }, + new UserReviewDto { Tagline = "3", Score = 1 }, + new UserReviewDto { Tagline = "4", Score = 4 }, + new UserReviewDto { Tagline = "5", Score = 2 } + }; + + // Act + var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList(); + + // Assert + Assert.Equal(5, result.Count); + Assert.Equal(5, result[0].Score); + Assert.Equal(4, result[1].Score); + Assert.Equal(3, result[2].Score); + Assert.Equal(2, result[3].Score); + Assert.Equal(1, result[4].Score); + } + + #endregion + + #region Helper Methods + + private static List CreateReviewList(int count) + { + var reviews = new List(); + for (var i = 0; i < count; i++) + { + reviews.Add(new UserReviewDto + { + Tagline = $"{i + 1}", + Score = count - i // This makes them ordered by score descending initially + }); + } + return reviews; + } + + #endregion +} + diff --git a/API.Tests/Helpers/ScannerHelper.cs b/Kavita.Services.Tests/Helpers/ScannerHelper.cs similarity index 95% rename from API.Tests/Helpers/ScannerHelper.cs rename to Kavita.Services.Tests/Helpers/ScannerHelper.cs index 7d5263661..be8ebba65 100644 --- a/API.Tests/Helpers/ScannerHelper.cs +++ b/Kavita.Services.Tests/Helpers/ScannerHelper.cs @@ -1,41 +1,39 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions; +using System.IO.Abstractions; using System.IO.Compression; -using System.Linq; using System.Text; using System.Text.Json; -using System.Threading.Tasks; using System.Xml; using System.Xml.Serialization; -using API.Data; -using API.Data.Metadata; -using API.Entities; -using API.Entities.Enums; -using API.Helpers; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; -using API.Services.Tasks; -using API.Services.Tasks.Metadata; -using API.Services.Tasks.Scanner; -using API.SignalR; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Helpers; +using Kavita.API.Services.Metadata; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +using Kavita.API.Services.Scanner; +using Kavita.API.Services.SignalR; +using Kavita.Database; +using Kavita.Models; +using Kavita.Models.Builders; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Services.Reading; +using Kavita.Services.Scanner; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit.Abstractions; -namespace API.Tests.Helpers; -#nullable enable +namespace Kavita.Services.Tests.Helpers; public class ScannerHelper { private readonly IUnitOfWork _unitOfWork; private readonly ITestOutputHelper _testOutputHelper; - private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/ScanTests"); - private readonly string _testcasesDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/TestCases"); - private readonly string _imagePath = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/1x1.png"); + private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ScannerService/ScanTests"); + private readonly string _testcasesDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ScannerService/TestCases"); + private readonly string _imagePath = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ScannerService/1x1.png"); private static readonly string[] ComicInfoExtensions = [".cbz", ".cbr", ".zip", ".rar"]; private static readonly string[] EpubExtensions = [".epub"]; @@ -55,7 +53,7 @@ public class ScannerHelper .WithFolders([new FolderPath() {Path = testDirectoryPath}]) .Build(); - var admin = new AppUserBuilder("admin", "admin@kavita.com", Seed.DefaultThemes[0]) + var admin = new AppUserBuilder("admin", "admin@kavita.com", Defaults.DefaultThemes[0]) .WithLibrary(library) .Build(); diff --git a/API.Tests/Helpers/SeriesHelperTests.cs b/Kavita.Services.Tests/Helpers/SeriesHelperTests.cs similarity index 96% rename from API.Tests/Helpers/SeriesHelperTests.cs rename to Kavita.Services.Tests/Helpers/SeriesHelperTests.cs index 22b4a3cd1..00f235b1e 100644 --- a/API.Tests/Helpers/SeriesHelperTests.cs +++ b/Kavita.Services.Tests/Helpers/SeriesHelperTests.cs @@ -1,14 +1,11 @@ -using System.Collections.Generic; -using System.Linq; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers; -using API.Helpers.Builders; -using API.Services.Tasks.Scanner; -using Xunit; +using Kavita.Common.Extensions; +using Kavita.Models.Builders; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Parser; +using Kavita.Services.Helpers; -namespace API.Tests.Helpers; +namespace Kavita.Services.Tests.Helpers; public class SeriesHelperTests { diff --git a/API.Tests/Helpers/SmartFilterHelperTests.cs b/Kavita.Services.Tests/Helpers/SmartFilterHelperTests.cs similarity index 62% rename from API.Tests/Helpers/SmartFilterHelperTests.cs rename to Kavita.Services.Tests/Helpers/SmartFilterHelperTests.cs index 974cb0ba6..651c0b5d0 100644 --- a/API.Tests/Helpers/SmartFilterHelperTests.cs +++ b/Kavita.Services.Tests/Helpers/SmartFilterHelperTests.cs @@ -1,29 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using API.Data.ManualMigrations; -using API.DTOs.Filtering; -using API.DTOs.Filtering.v2; -using API.Entities.Enums; -using API.Helpers; -using Xunit; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Helpers; -namespace API.Tests.Helpers; +namespace Kavita.Services.Tests.Helpers; public class SmartFilterHelperTests { - [Theory] - [InlineData("", false)] - [InlineData("name=DC%20-%20On%20Deck&stmts=comparison%3D1%26field%3D20%26value%3D0,comparison%3D9%26field%3D20%26value%3D100,comparison%3D0%26field%3D19%26value%3D274&sortOptions=sortField%3D1&isAscending=True&limitTo=0&combination=1", true)] - [InlineData("name=English%20In%20Progress&stmts=comparison%253D8%252Cfield%253D7%252Cvalue%253D4%25252C3,comparison%253D3%252Cfield%253D20%252Cvalue%253D100,comparison%253D8%252Cfield%253D3%252Cvalue%253Dja,comparison%253D1%252Cfield%253D20%252Cvalue%253D0&sortOptions=sortField%3D7,isAscending%3DFalse&limitTo=0&combination=1", true)] - [InlineData("name=Unread%20Isekai%20Light%20Novels&stmts=comparison%253D0%25C2%25A6field%253D20%25C2%25A6value%253D0%EF%BF%BDcomparison%253D5%25C2%25A6field%253D6%25C2%25A6value%253D230%EF%BF%BDcomparison%253D8%25C2%25A6field%253D7%25C2%25A6value%253D4%EF%BF%BDcomparison%253D0%25C2%25A6field%253D19%25C2%25A6value%253D14&sortOptions=sortField%3D5%C2%A6isAscending%3DFalse&limitTo=0&combination=1", false)] - [InlineData("name=Zero&stmts=comparison%3d7%26field%3d1%26value%3d0&sortOptions=sortField=2&isAscending=False&limitTo=0&combination=1", true)] - public void Test_ShouldMigrateFilter(string filter, bool expected) - { - Assert.Equal(expected, MigrateSmartFilterEncoding.ShouldMigrateFilter(filter)); - } - [Fact] public void Test_Decode() { @@ -126,24 +110,6 @@ public class SmartFilterHelperTests Assert.False(decoded.SortOptions.IsAscending); } - [Theory] - [InlineData("name=DC%20-%20On%20Deck&stmts=comparison%3D1%26field%3D20%26value%3D0,comparison%3D9%26field%3D20%26value%3D100,comparison%3D0%26field%3D19%26value%3D274&sortOptions=sortField%3D1&isAscending=True&limitTo=0&combination=1")] - [InlineData("name=Manga%20-%20On%20Deck&stmts=comparison%253D1%252Cfield%253D20%252Cvalue%253D0,comparison%253D3%252Cfield%253D20%252Cvalue%253D100,comparison%253D0%252Cfield%253D19%252Cvalue%253D2&sortOptions=sortField%3D1,isAscending%3DTrue&limitTo=0&combination=1")] - [InlineData("name=English%20In%20Progress&stmts=comparison%253D8%252Cfield%253D7%252Cvalue%253D4%25252C3,comparison%253D3%252Cfield%253D20%252Cvalue%253D100,comparison%253D8%252Cfield%253D3%252Cvalue%253Dja,comparison%253D1%252Cfield%253D20%252Cvalue%253D0&sortOptions=sortField%3D7,isAscending%3DFalse&limitTo=0&combination=1")] - public void MigrationWorks(string filter) - { - try - { - var updatedFilter = MigrateSmartFilterEncoding.EncodeFix(filter); - Assert.NotNull(updatedFilter); - } - catch (Exception ex) - { - Assert.Fail("Exception thrown: " + ex.Message); - } - - } - private static void AssertStatementSame(FilterStatementDto statement, FilterStatementDto statement2) { Assert.Equal(statement.Field, statement2.Field); diff --git a/API.Tests/Helpers/TestCaseGenerator.cs b/Kavita.Services.Tests/Helpers/TestCaseGenerator.cs similarity index 97% rename from API.Tests/Helpers/TestCaseGenerator.cs rename to Kavita.Services.Tests/Helpers/TestCaseGenerator.cs index 833da0502..58e4c5322 100644 --- a/API.Tests/Helpers/TestCaseGenerator.cs +++ b/Kavita.Services.Tests/Helpers/TestCaseGenerator.cs @@ -1,6 +1,4 @@ -using System.IO; - -namespace API.Tests.Helpers; +namespace Kavita.Services.Tests.Helpers; /// /// Given a -testcase.txt file, will generate a folder with fake archive or book files. These files are just renamed txt files. diff --git a/API.Tests/Services/ImageServiceTests.cs b/Kavita.Services.Tests/ImageServiceTests.cs similarity index 96% rename from API.Tests/Services/ImageServiceTests.cs rename to Kavita.Services.Tests/ImageServiceTests.cs index f2c87e1ad..bfa586da0 100644 --- a/API.Tests/Services/ImageServiceTests.cs +++ b/Kavita.Services.Tests/ImageServiceTests.cs @@ -1,18 +1,14 @@ -using System.IO; -using System.Linq; -using System.Text; -using API.Entities.Enums; -using API.Services; +using System.Text; +using Kavita.Models.Entities.Enums; using NetVips; -using Xunit; using Image = NetVips.Image; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class ImageServiceTests { - private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ImageService/Covers"); - private readonly string _testDirectoryColorScapes = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ImageService/ColorScapes"); + private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ImageService/Covers"); + private readonly string _testDirectoryColorScapes = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ImageService/ColorScapes"); private const string OutputPattern = "_output"; private const string BaselinePattern = "_baseline"; diff --git a/Kavita.Services.Tests/Kavita.Services.Tests.csproj b/Kavita.Services.Tests/Kavita.Services.Tests.csproj new file mode 100644 index 000000000..15b334b9d --- /dev/null +++ b/Kavita.Services.Tests/Kavita.Services.Tests.csproj @@ -0,0 +1,42 @@ + + + + net10.0 + enable + enable + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + PreserveNewest + + + I18N\%(RecursiveDir)%(FileName)%(Extension) + PreserveNewest + + + + diff --git a/API.Tests/Services/MetadataServiceTests.cs b/Kavita.Services.Tests/MetadataServiceTests.cs similarity index 81% rename from API.Tests/Services/MetadataServiceTests.cs rename to Kavita.Services.Tests/MetadataServiceTests.cs index 01a084242..1a6737cb2 100644 --- a/API.Tests/Services/MetadataServiceTests.cs +++ b/Kavita.Services.Tests/MetadataServiceTests.cs @@ -1,18 +1,15 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions.TestingHelpers; -using API.Helpers; -using API.Services; +using System.IO.Abstractions.TestingHelpers; +using Kavita.API.Services.Helpers; +using Kavita.Services.Helpers; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class MetadataServiceTests { - private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/Archives"); + private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ArchiveService/Archives"); private const string TestCoverImageFile = "thumbnail.jpg"; private const string TestCoverArchive = @"c:\file in folder.zip"; - private readonly string _testCoverImageDirectory = Path.Join(Directory.GetCurrentDirectory(), @"../../../Services/Test Data/ArchiveService/CoverImages"); + private readonly string _testCoverImageDirectory = Path.Join(Directory.GetCurrentDirectory(), @"../../../Test Data/ArchiveService/CoverImages"); //private readonly MetadataService _metadataService; // private readonly IUnitOfWork _unitOfWork = Substitute.For(); // private readonly IImageService _imageService = Substitute.For(); diff --git a/API.Tests/Services/OidcServiceTests.cs b/Kavita.Services.Tests/OidcServiceTests.cs similarity index 98% rename from API.Tests/Services/OidcServiceTests.cs rename to Kavita.Services.Tests/OidcServiceTests.cs index 13dafd6d7..9cd859bb3 100644 --- a/API.Tests/Services/OidcServiceTests.cs +++ b/Kavita.Services.Tests/OidcServiceTests.cs @@ -1,19 +1,18 @@ -using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; -using System.Linq; using System.Security.Claims; -using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.Settings; -using API.Entities; -using API.Entities.Enums; -using API.Helpers.Builders; -using API.Services; -using API.Services.Tasks.Metadata; -using API.Services.Tasks.Scanner; using AutoMapper; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Metadata; +using Kavita.API.Services.Scanner; using Kavita.Common; +using Kavita.Database; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; @@ -21,10 +20,9 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class OidcServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { @@ -645,7 +643,8 @@ public class OidcServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(ou Substitute.For(), Substitute.For(), Substitute.For>(), - Substitute.For() + Substitute.For(), + Substitute.For() ); private static ClaimsPrincipal BuildPrincipal(IEnumerable claims) diff --git a/API.Tests/Services/OpdsServiceTests.cs b/Kavita.Services.Tests/OpdsServiceTests.cs similarity index 97% rename from API.Tests/Services/OpdsServiceTests.cs rename to Kavita.Services.Tests/OpdsServiceTests.cs index 8b1602369..88425af46 100644 --- a/API.Tests/Services/OpdsServiceTests.cs +++ b/Kavita.Services.Tests/OpdsServiceTests.cs @@ -1,37 +1,38 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.OPDS; -using API.DTOs.OPDS.Requests; -using API.DTOs.Progress; -using API.Constants; -using API.Entities; -using API.Entities.Enums; -using API.Helpers; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; -using API.Services.Reading; -using API.SignalR; +using System.IO.Abstractions; using AutoMapper; using Hangfire; using Hangfire.InMemory; +using Kavita.API.Database; +using Kavita.API.Errors; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; +using Kavita.Common.Helpers; +using Kavita.Database; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.OPDS; +using Kavita.Models.DTOs.OPDS.Requests; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; +using Kavita.Services.Reading; +using Kavita.Services.Scanner; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class OpdsServiceTests(ITestOutputHelper testOutputHelper) : AbstractDbTest(testOutputHelper) { - private readonly string _testFilePath = Path.Join(Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/OpdsService"), "test.zip"); + private readonly string _testFilePath = Path.Join(Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/OpdsService"), "test.zip"); #region Setup @@ -89,7 +90,7 @@ public class OpdsServiceTests(ITestOutputHelper testOutputHelper) : AbstractDbTe foreach (var i in Enumerable.Range(0, numberOfSeries)) { var series = new SeriesBuilder("Test " + (i + 1)) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1") .WithSortOrder(counter) .WithPages(10) @@ -411,7 +412,7 @@ public class OpdsServiceTests(ITestOutputHelper testOutputHelper) : AbstractDbTe var (opdsService, _) = SetupService(unitOfWork, mapper); var user = await SetupSeriesAndUser(context, unitOfWork); - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => { await opdsService.Search(new OpdsSearchRequest { diff --git a/API.Tests/Services/ParseScannedFilesTests.cs b/Kavita.Services.Tests/ParseScannedFilesTests.cs similarity index 98% rename from API.Tests/Services/ParseScannedFilesTests.cs rename to Kavita.Services.Tests/ParseScannedFilesTests.cs index 2c064eb75..20d8bab8b 100644 --- a/API.Tests/Services/ParseScannedFilesTests.cs +++ b/Kavita.Services.Tests/ParseScannedFilesTests.cs @@ -1,26 +1,21 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions; +using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Metadata; -using API.Data.Repositories; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; -using API.Tests.Helpers; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; +using Kavita.Database.Tests; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Models.Parser; +using Kavita.Services.Scanner; +using Kavita.Services.Tests.Helpers; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class MockReadingItemService : IReadingItemService { diff --git a/API.Tests/Parsers/BasicParserTests.cs b/Kavita.Services.Tests/Parsers/BasicParserTests.cs similarity index 97% rename from API.Tests/Parsers/BasicParserTests.cs rename to Kavita.Services.Tests/Parsers/BasicParserTests.cs index 32673e0e6..dacf96ed8 100644 --- a/API.Tests/Parsers/BasicParserTests.cs +++ b/Kavita.Services.Tests/Parsers/BasicParserTests.cs @@ -1,13 +1,11 @@ -using System.IO; -using System.IO.Abstractions.TestingHelpers; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using System.IO.Abstractions.TestingHelpers; +using Kavita.Database.Tests; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; -namespace API.Tests.Parsers; +namespace Kavita.Services.Tests.Parsers; public class BasicParserTests : AbstractFsTest { diff --git a/API.Tests/Parsers/BookParserTests.cs b/Kavita.Services.Tests/Parsers/BookParserTests.cs similarity index 94% rename from API.Tests/Parsers/BookParserTests.cs rename to Kavita.Services.Tests/Parsers/BookParserTests.cs index 90147ac6b..8a0e13d75 100644 --- a/API.Tests/Parsers/BookParserTests.cs +++ b/Kavita.Services.Tests/Parsers/BookParserTests.cs @@ -1,12 +1,11 @@ using System.IO.Abstractions.TestingHelpers; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using Kavita.API.Services; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; -namespace API.Tests.Parsers; +namespace Kavita.Services.Tests.Parsers; public class BookParserTests { diff --git a/API.Tests/Parsers/ComicVineParserTests.cs b/Kavita.Services.Tests/Parsers/ComicVineParserTests.cs similarity index 96% rename from API.Tests/Parsers/ComicVineParserTests.cs rename to Kavita.Services.Tests/Parsers/ComicVineParserTests.cs index 2f4fd568e..202906a17 100644 --- a/API.Tests/Parsers/ComicVineParserTests.cs +++ b/Kavita.Services.Tests/Parsers/ComicVineParserTests.cs @@ -1,13 +1,11 @@ using System.IO.Abstractions.TestingHelpers; -using API.Data.Metadata; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; -namespace API.Tests.Parsers; +namespace Kavita.Services.Tests.Parsers; public class ComicVineParserTests { diff --git a/API.Tests/Parsers/DefaultParserTests.cs b/Kavita.Services.Tests/Parsers/DefaultParserTests.cs similarity index 99% rename from API.Tests/Parsers/DefaultParserTests.cs rename to Kavita.Services.Tests/Parsers/DefaultParserTests.cs index ffe14a7c3..a26227772 100644 --- a/API.Tests/Parsers/DefaultParserTests.cs +++ b/Kavita.Services.Tests/Parsers/DefaultParserTests.cs @@ -1,14 +1,12 @@ -using System.Collections.Generic; -using System.IO.Abstractions.TestingHelpers; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using System.IO.Abstractions.TestingHelpers; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Parser; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Parsers; +namespace Kavita.Services.Tests.Parsers; public class DefaultParserTests { diff --git a/API.Tests/Parsers/ImageParserTests.cs b/Kavita.Services.Tests/Parsers/ImageParserTests.cs similarity index 96% rename from API.Tests/Parsers/ImageParserTests.cs rename to Kavita.Services.Tests/Parsers/ImageParserTests.cs index 63df1926e..21c7b8df0 100644 --- a/API.Tests/Parsers/ImageParserTests.cs +++ b/Kavita.Services.Tests/Parsers/ImageParserTests.cs @@ -1,12 +1,10 @@ using System.IO.Abstractions.TestingHelpers; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; -namespace API.Tests.Parsers; +namespace Kavita.Services.Tests.Parsers; public class ImageParserTests { diff --git a/API.Tests/Parsers/PdfParserTests.cs b/Kavita.Services.Tests/Parsers/PdfParserTests.cs similarity index 95% rename from API.Tests/Parsers/PdfParserTests.cs rename to Kavita.Services.Tests/Parsers/PdfParserTests.cs index 08bf9f25d..2d6e5e59b 100644 --- a/API.Tests/Parsers/PdfParserTests.cs +++ b/Kavita.Services.Tests/Parsers/PdfParserTests.cs @@ -1,12 +1,10 @@ using System.IO.Abstractions.TestingHelpers; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; -namespace API.Tests.Parsers; +namespace Kavita.Services.Tests.Parsers; public class PdfParserTests { diff --git a/API.Tests/Parsing/BookParsingTests.cs b/Kavita.Services.Tests/Parsing/BookParsingTests.cs similarity index 70% rename from API.Tests/Parsing/BookParsingTests.cs rename to Kavita.Services.Tests/Parsing/BookParsingTests.cs index 9b02eff63..136d0aae5 100644 --- a/API.Tests/Parsing/BookParsingTests.cs +++ b/Kavita.Services.Tests/Parsing/BookParsingTests.cs @@ -1,7 +1,7 @@ -using API.Entities.Enums; -using Xunit; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Scanner; -namespace API.Tests.Parsing; +namespace Kavita.Services.Tests.Parsing; public class BookParsingTests { @@ -11,7 +11,7 @@ public class BookParsingTests [InlineData("Faust - Volume 01 [Del Rey][Scans_Compressed]", "Faust")] public void ParseSeriesTest(string filename, string expected) { - Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename, LibraryType.Book)); + Assert.Equal(expected, Parser.ParseSeries(filename, LibraryType.Book)); } [Theory] @@ -19,6 +19,6 @@ public class BookParsingTests [InlineData("Faust - Volume 01 [Del Rey][Scans_Compressed]", "1")] public void ParseVolumeTest(string filename, string expected) { - Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename, LibraryType.Book)); + Assert.Equal(expected, Parser.ParseVolume(filename, LibraryType.Book)); } } diff --git a/API.Tests/Parsing/ComicParsingTests.cs b/Kavita.Services.Tests/Parsing/ComicParsingTests.cs similarity index 99% rename from API.Tests/Parsing/ComicParsingTests.cs rename to Kavita.Services.Tests/Parsing/ComicParsingTests.cs index a0375a566..1f2900ea9 100644 --- a/API.Tests/Parsing/ComicParsingTests.cs +++ b/Kavita.Services.Tests/Parsing/ComicParsingTests.cs @@ -1,8 +1,7 @@ -using API.Entities.Enums; -using API.Services.Tasks.Scanner.Parser; -using Xunit; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Scanner; -namespace API.Tests.Parsing; +namespace Kavita.Services.Tests.Parsing; public class ComicParsingTests { diff --git a/API.Tests/Parsing/ImageParsingTests.cs b/Kavita.Services.Tests/Parsing/ImageParsingTests.cs similarity index 95% rename from API.Tests/Parsing/ImageParsingTests.cs rename to Kavita.Services.Tests/Parsing/ImageParsingTests.cs index 362b4b08c..00165e9c4 100644 --- a/API.Tests/Parsing/ImageParsingTests.cs +++ b/Kavita.Services.Tests/Parsing/ImageParsingTests.cs @@ -1,13 +1,12 @@ using System.IO.Abstractions.TestingHelpers; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Parser; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Parsing; +namespace Kavita.Services.Tests.Parsing; public class ImageParsingTests { @@ -30,7 +29,7 @@ public class ImageParsingTests var filepath = @"E:\Manga\Monster #8\Ch. 001-016 [MangaPlus] [Digital] [amit34521]\Monster #8 Ch. 001 [MangaPlus] [Digital] [amit34521]\13.jpg"; var expectedInfo2 = new ParserInfo { - Series = "Monster #8", Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, Edition = "", + Series = "Monster #8", Volumes = Parser.LooseLeafVolume, Edition = "", Chapters = "8", Filename = "13.jpg", Format = MangaFormat.Image, FullFilePath = filepath, IsSpecial = false }; diff --git a/API.Tests/Parsing/MangaParsingTests.cs b/Kavita.Services.Tests/Parsing/MangaParsingTests.cs similarity index 99% rename from API.Tests/Parsing/MangaParsingTests.cs rename to Kavita.Services.Tests/Parsing/MangaParsingTests.cs index 8a983e899..bc575e9e0 100644 --- a/API.Tests/Parsing/MangaParsingTests.cs +++ b/Kavita.Services.Tests/Parsing/MangaParsingTests.cs @@ -1,8 +1,7 @@ -using API.Entities.Enums; -using API.Services.Tasks.Scanner.Parser; -using Xunit; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Scanner; -namespace API.Tests.Parsing; +namespace Kavita.Services.Tests.Parsing; public class MangaParsingTests { diff --git a/API.Tests/Parsing/ParserInfoTests.cs b/Kavita.Services.Tests/Parsing/ParserInfoTests.cs similarity index 94% rename from API.Tests/Parsing/ParserInfoTests.cs rename to Kavita.Services.Tests/Parsing/ParserInfoTests.cs index cbb8ae99a..fd6c3d73d 100644 --- a/API.Tests/Parsing/ParserInfoTests.cs +++ b/Kavita.Services.Tests/Parsing/ParserInfoTests.cs @@ -1,8 +1,9 @@ -using API.Entities.Enums; -using API.Services.Tasks.Scanner.Parser; -using Xunit; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Parser; +using Kavita.Services.Extensions; +using Kavita.Services.Scanner; -namespace API.Tests.Parsing; +namespace Kavita.Services.Tests.Parsing; public class ParserInfoTests { diff --git a/API.Tests/Parsing/ParsingTests.cs b/Kavita.Services.Tests/Parsing/ParsingTests.cs similarity index 98% rename from API.Tests/Parsing/ParsingTests.cs rename to Kavita.Services.Tests/Parsing/ParsingTests.cs index 5a91ab281..bce33b68d 100644 --- a/API.Tests/Parsing/ParsingTests.cs +++ b/Kavita.Services.Tests/Parsing/ParsingTests.cs @@ -1,10 +1,7 @@ using System.Globalization; -using System.Linq; -using API.Services.Tasks.Scanner.Parser; -using Xunit; -using static API.Services.Tasks.Scanner.Parser.Parser; +using static Kavita.Services.Scanner.Parser; -namespace API.Tests.Parsing; +namespace Kavita.Services.Tests.Parsing; public class ParsingTests { diff --git a/API.Tests/Services/PersonServiceTests.cs b/Kavita.Services.Tests/PersonServiceTests.cs similarity index 97% rename from API.Tests/Services/PersonServiceTests.cs rename to Kavita.Services.Tests/PersonServiceTests.cs index 3a4a1e4f7..efec0f914 100644 --- a/API.Tests/Services/PersonServiceTests.cs +++ b/Kavita.Services.Tests/PersonServiceTests.cs @@ -1,15 +1,13 @@ -using System.Linq; -using System.Threading.Tasks; -using API.Data.Repositories; -using API.Entities.Enums; -using API.Entities.Person; -using API.Extensions; -using API.Helpers.Builders; -using API.Services; -using Xunit; +using Kavita.API.Repositories; +using Kavita.Common.Extensions; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Person; +using Kavita.Services.Builders; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class PersonServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { diff --git a/API.Tests/Services/ProcessSeriesTests.cs b/Kavita.Services.Tests/ProcessSeriesTests.cs similarity index 90% rename from API.Tests/Services/ProcessSeriesTests.cs rename to Kavita.Services.Tests/ProcessSeriesTests.cs index 119e1bc10..ede59e433 100644 --- a/API.Tests/Services/ProcessSeriesTests.cs +++ b/Kavita.Services.Tests/ProcessSeriesTests.cs @@ -1,4 +1,4 @@ -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class ProcessSeriesTests { @@ -33,7 +33,7 @@ public class ProcessSeriesTests // public void UpdateChapterFromComicInfo_() // { // // TODO: Do this - // var file = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/Library/Manga/Hajime no Ippo/Hajime no Ippo Chapter 1.cbz"); + // var file = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ScannerService/Library/Manga/Hajime no Ippo/Hajime no Ippo Chapter 1.cbz"); // // Chapter and ComicInfo // var chapter = new ChapterBuilder("1") // .WithId(0) diff --git a/API.Tests/Services/RatingServiceTests.cs b/Kavita.Services.Tests/RatingServiceTests.cs similarity index 95% rename from API.Tests/Services/RatingServiceTests.cs rename to Kavita.Services.Tests/RatingServiceTests.cs index e10056380..3c93225d8 100644 --- a/API.Tests/Services/RatingServiceTests.cs +++ b/Kavita.Services.Tests/RatingServiceTests.cs @@ -1,19 +1,17 @@ -using System.Linq; -using System.Threading.Tasks; -using API.Data.Repositories; -using API.DTOs; -using API.Entities.Enums; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; using Hangfire; using Hangfire.InMemory; +using Kavita.API.Repositories; +using Kavita.API.Services.Plus; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.DTOs; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Builders; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class RatingServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { diff --git a/API.Tests/Services/ReaderServiceRereadTests.cs b/Kavita.Services.Tests/ReaderServiceRereadTests.cs similarity index 98% rename from API.Tests/Services/ReaderServiceRereadTests.cs rename to Kavita.Services.Tests/ReaderServiceRereadTests.cs index 99ac8d72c..9e9477b9e 100644 --- a/API.Tests/Services/ReaderServiceRereadTests.cs +++ b/Kavita.Services.Tests/ReaderServiceRereadTests.cs @@ -1,22 +1,18 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.DTOs; -using API.Entities.Enums; -using API.Entities.User; -using API.Services.Reading; -using API.Data; -using API.Data.Repositories; -using API.Services; -using API.Services.Plus; -using API.SignalR; +using System.IO.Abstractions.TestingHelpers; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; +using Kavita.Models.DTOs; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Services.Reading; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; -using System.IO.Abstractions.TestingHelpers; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class ReaderServiceRereadTests { diff --git a/API.Tests/Services/ReaderServiceTests.cs b/Kavita.Services.Tests/ReaderServiceTests.cs similarity index 88% rename from API.Tests/Services/ReaderServiceTests.cs rename to Kavita.Services.Tests/ReaderServiceTests.cs index 306f7c78f..912555146 100644 --- a/API.Tests/Services/ReaderServiceTests.cs +++ b/Kavita.Services.Tests/ReaderServiceTests.cs @@ -1,29 +1,29 @@ -using System.Collections.Generic; -using System.IO.Abstractions.TestingHelpers; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Progress; -using API.DTOs.Reader; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; -using API.Services.Reading; -using API.SignalR; +using System.IO.Abstractions.TestingHelpers; using Hangfire; using Hangfire.InMemory; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; +using Kavita.Common.Extensions; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; +using Kavita.Services.Reading; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -using YamlDotNet.Core; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDbTest(testOutputHelper) { @@ -65,8 +65,8 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithPages(1) .Build()) .Build()) @@ -102,8 +102,8 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithPages(1) .Build()) .Build()) @@ -147,8 +147,8 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithPages(1) .Build()) .Build()) @@ -211,11 +211,11 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithPages(1) .Build()) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithPages(2) .Build()) .Build()) @@ -257,11 +257,11 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithPages(1) .Build()) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithPages(2) .Build()) .Build()) @@ -358,7 +358,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) .WithVolume(new VolumeBuilder("1-2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).Build()) .Build()) .WithVolume(new VolumeBuilder("3-4") @@ -536,7 +536,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("22").Build()) .Build()) @@ -576,7 +576,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("66").Build()) .WithChapter(new ChapterBuilder("67").Build()) .Build()) @@ -586,7 +586,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).Build()) .Build()) .Build(); @@ -607,7 +607,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb Assert.NotEqual(-1, nextChapter); var actualChapter = await unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); Assert.NotNull(actualChapter); - Assert.Equal(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, actualChapter.Range); + Assert.Equal(Parser.DefaultChapter, actualChapter.Range); } [Fact] @@ -627,9 +627,9 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .WithChapter(new ChapterBuilder("2").Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber).Build()) - .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).Build()) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber).Build()) + .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 1).Build()) .Build()) .Build(); @@ -689,7 +689,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) @@ -721,12 +721,12 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).Build()) .Build()) .Build(); @@ -755,15 +755,15 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .Build()) .Build()) @@ -804,14 +804,14 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .WithChapter(new ChapterBuilder("2").Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) .WithChapter(new ChapterBuilder("A.cbz") .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .Build()) .WithChapter(new ChapterBuilder("B.cbz") .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 2) + .WithSortOrder(Parser.SpecialVolumeNumber + 2) .Build()) .Build()) .Build(); @@ -846,15 +846,15 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) .WithChapter(new ChapterBuilder("A.cbz") .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .WithPages(1) .Build()) .Build()) @@ -890,19 +890,19 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) .WithChapter(new ChapterBuilder("A.cbz") .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .WithPages(1) .Build()) .Build()) @@ -939,14 +939,14 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) .WithChapter(new ChapterBuilder("A.cbz") .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .Build()) .WithChapter(new ChapterBuilder("B.cbz") .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 2) + .WithSortOrder(Parser.SpecialVolumeNumber + 2) .Build()) .Build()) .Build(); @@ -1117,16 +1117,16 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("40").WithPages(1).Build()) .WithChapter(new ChapterBuilder("50").WithPages(1).Build()) .WithChapter(new ChapterBuilder("60").WithPages(1).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) .WithChapter(new ChapterBuilder("Some Special Title") .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .WithPages(1) .Build()) .Build()) @@ -1226,9 +1226,9 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .WithChapter(new ChapterBuilder("2").Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).Build()) - .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 2).Build()) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 1).Build()) + .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 2).Build()) .Build()) .Build(); @@ -1296,7 +1296,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).Build()) .Build()) .WithLibraryId(library.Id) .Build(); @@ -1328,13 +1328,13 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb await context.SaveChangesAsync(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).Build()) .Build()) .WithLibraryId(library.Id) .Build(); @@ -1363,7 +1363,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb await context.SaveChangesAsync(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("5").Build()) .WithChapter(new ChapterBuilder("6").Build()) .WithChapter(new ChapterBuilder("7").Build()) @@ -1413,7 +1413,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb await context.SaveChangesAsync(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) @@ -1451,14 +1451,14 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) .WithChapter(new ChapterBuilder("A.cbz") .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .Build()) .WithChapter(new ChapterBuilder("B.cbz") .WithIsSpecial(true) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 2) + .WithSortOrder(Parser.SpecialVolumeNumber + 2) .Build()) .Build()) .WithLibraryId(library.Id) @@ -1496,7 +1496,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb await context.SaveChangesAsync(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) @@ -1573,7 +1573,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb await context.SaveChangesAsync(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("95").Build()) .WithChapter(new ChapterBuilder("96").Build()) .Build()) @@ -1625,7 +1625,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .WithChapter(new ChapterBuilder("1").WithPages(3).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) .Build()) .WithPages(4) .WithLibraryId(library.Id) @@ -1671,7 +1671,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .WithChapter(new ChapterBuilder("1", "1-11").WithPages(3).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) .Build()) .WithPages(4) .WithLibraryId(library.Id) @@ -1790,21 +1790,21 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") // Loose chapters - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("45").WithPages(1).Build()) .WithChapter(new ChapterBuilder("46").WithPages(1).Build()) .WithChapter(new ChapterBuilder("47").WithPages(1).Build()) .WithChapter(new ChapterBuilder("48").WithPages(1).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) .WithChapter(new ChapterBuilder("Some Special Title") - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1) + .WithSortOrder(Parser.SpecialVolumeNumber + 1) .WithIsSpecial(true).WithPages(1) .Build()) .Build()) // One file volume .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) // Read + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) // Read .Build()) // Chapter-based volume .WithVolume(new VolumeBuilder("2") @@ -1872,12 +1872,12 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") // Loose chapters - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Prologue").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithPages(1).Build()) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder("Prologue").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 1).WithPages(1).Build()) .Build()) .WithLibraryId(library.Id) .Build(); @@ -1910,23 +1910,23 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") // Loose chapters .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) + .WithSortOrder(Parser.DefaultChapterNumber).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) + .WithSortOrder(Parser.DefaultChapterNumber).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("12") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) + .WithSortOrder(Parser.DefaultChapterNumber).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("99.9") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) - .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) + .WithSortOrder(Parser.DefaultChapterNumber).WithPages(1).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, "Short Stories").WithIsSpecial(true) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter, "Short Stories").WithIsSpecial(true) .WithSortOrder(0).WithPages(1).Build()) .Build()) .WithLibraryId(library.Id) @@ -1966,7 +1966,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .WithVolume(new VolumeBuilder("2") .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("31").WithPages(1).Build()) .WithChapter(new ChapterBuilder("32").WithPages(1).Build()) .Build()) @@ -2029,7 +2029,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb await context.SaveChangesAsync(); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("230").WithPages(1).Build()) .WithChapter(new ChapterBuilder("231").WithPages(1).Build()) .Build()) @@ -2073,19 +2073,19 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("100").WithPages(1).Build()) .WithChapter(new ChapterBuilder("101").WithPages(1).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Christmas Eve").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithPages(1).Build()) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder("Christmas Eve").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 1).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) .Build()) .Build(); @@ -2135,7 +2135,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("100").WithPages(1).Build()) .WithChapter(new ChapterBuilder("101").WithPages(1).Build()) .WithChapter(new ChapterBuilder("102").WithPages(1).Build()) @@ -2270,7 +2270,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) .WithChapter(new ChapterBuilder("3").WithPages(1).Build()) @@ -2317,13 +2317,13 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) .WithChapter(new ChapterBuilder("3").WithPages(1).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Some Special Title").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithPages(1).Build()) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder("Some Special Title").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 1).WithPages(1).Build()) .Build()) .Build(); @@ -2385,7 +2385,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("230").WithPages(1).Build()) //.WithChapter(new ChapterBuilder("231").WithPages(1).Build()) (Added later) .Build()) @@ -2395,7 +2395,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) //.WithChapter(new ChapterBuilder("14.9").WithPages(1).Build()) (added later) .Build()) .Build(); @@ -2442,13 +2442,13 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb context.Library.Add(library); await context.SaveChangesAsync(); - var readChapter1 = new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build(); - var readChapter2 = new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build(); - var volume = new VolumeBuilder("3").WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()).Build(); + var readChapter1 = new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build(); + var readChapter2 = new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build(); + var volume = new VolumeBuilder("3").WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()).Build(); var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("51").WithPages(1).Build()) .WithChapter(new ChapterBuilder("52").WithPages(1).Build()) .WithChapter(new ChapterBuilder("53").WithPages(1).Build()) @@ -2462,7 +2462,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .Build()) // 3, 4, and all loose leafs are unread should be unread .WithVolume(new VolumeBuilder("3") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("4") .WithChapter(new ChapterBuilder("40").WithPages(1).Build()) @@ -2523,13 +2523,13 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) .WithChapter(new ChapterBuilder("22").WithPages(1).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("51").WithPages(1).Build()) .WithChapter(new ChapterBuilder("52").WithPages(1).Build()) .WithChapter(new ChapterBuilder("91").WithPages(2).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Special").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithPages(1).Build()) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder("Special").WithIsSpecial(true).WithSortOrder(Parser.SpecialVolumeNumber + 1).WithPages(1).Build()) .Build()) .Build(); @@ -2718,13 +2718,13 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) .WithChapter(new ChapterBuilder("3").WithPages(1).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(1).Build()) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(1).Build()) .Build()) .Build(); @@ -2764,14 +2764,14 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) .WithChapter(new ChapterBuilder("2.5").WithPages(1).Build()) .WithChapter(new ChapterBuilder("3").WithPages(1).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(1).Build()) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(1).Build()) .Build()) .Build(); @@ -2813,10 +2813,10 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .WithLibraryId(library.Id) .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) .Build()) .Build(); @@ -2854,7 +2854,7 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("45").WithPages(5).Build()) .WithChapter(new ChapterBuilder("46").WithPages(46).Build()) .WithChapter(new ChapterBuilder("47").WithPages(47).Build()) @@ -2862,15 +2862,15 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb .WithChapter(new ChapterBuilder("49").WithPages(49).Build()) .WithChapter(new ChapterBuilder("50").WithPages(50).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(10).Build()) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(10).Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(6).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(6).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(7).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(7).Build()) .Build()) .WithVolume(new VolumeBuilder("3") .WithChapter(new ChapterBuilder("12").WithPages(5).Build()) @@ -2929,11 +2929,11 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) .WithChapter(new ChapterBuilder("1").WithPages(2).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) .WithChapter(new ChapterBuilder("1").WithPages(2).Build()) .Build()) .Build(); @@ -2974,8 +2974,8 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) .WithChapter(new ChapterBuilder("1").WithPages(2).Build()) .Build()) .Build(); @@ -3054,24 +3054,24 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("10").WithPages(1).Build()) .WithChapter(new ChapterBuilder("20").WithPages(1).Build()) .WithChapter(new ChapterBuilder("30").WithPages(1).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(1).Build()) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("1997") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("2002") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("2003") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter).WithPages(1).Build()) .Build()) .Build(); @@ -3117,13 +3117,13 @@ public class ReaderServiceTests(ITestOutputHelper testOutputHelper) : AbstractDb var series = new SeriesBuilder("Test") .WithLibraryId(library.Id) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("10").WithPages(1).Build()) .WithChapter(new ChapterBuilder("20").WithPages(1).Build()) .WithChapter(new ChapterBuilder("30").WithPages(1).Build()) .Build()) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume) - .WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(1).Build()) + .WithVolume(new VolumeBuilder(Parser.SpecialVolume) + .WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(1).Build()) .Build()) .WithVolume(new VolumeBuilder("1997") .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) diff --git a/API.Tests/Services/ReadingHistoryServiceTests.cs b/Kavita.Services.Tests/ReadingHistoryServiceTests.cs similarity index 90% rename from API.Tests/Services/ReadingHistoryServiceTests.cs rename to Kavita.Services.Tests/ReadingHistoryServiceTests.cs index c1c340429..3507d5ffe 100644 --- a/API.Tests/Services/ReadingHistoryServiceTests.cs +++ b/Kavita.Services.Tests/ReadingHistoryServiceTests.cs @@ -1,22 +1,20 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Progress; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Progress; -using API.Extensions.QueryExtensions; -using API.Helpers.Builders; -using API.Services.Reading; +using Kavita.API.Repositories; +using Kavita.Database; +using Kavita.Database.Extensions; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; +using Kavita.Services.Reading; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class ReadingHistoryServiceTests(ITestOutputHelper testOutputHelper) : AbstractDbTest(testOutputHelper) { diff --git a/API.Tests/Services/ReadingListServiceTests.cs b/Kavita.Services.Tests/ReadingListServiceTests.cs similarity index 96% rename from API.Tests/Services/ReadingListServiceTests.cs rename to Kavita.Services.Tests/ReadingListServiceTests.cs index 1443d6fb4..d8ddb21d6 100644 --- a/API.Tests/Services/ReadingListServiceTests.cs +++ b/Kavita.Services.Tests/ReadingListServiceTests.cs @@ -1,27 +1,27 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions.TestingHelpers; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.ReadingLists; -using API.DTOs.ReadingLists.CBL; -using API.Entities; -using API.Entities.Enums; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; -using API.Services.Reading; -using API.SignalR; +using System.IO.Abstractions.TestingHelpers; using AutoMapper; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; +using Kavita.Database; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.DTOs.ReadingLists.CBL; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; +using Kavita.Services.Reading; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { @@ -54,7 +54,7 @@ public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDb .WithMetadata(new SeriesMetadataBuilder().Build()) .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1") .WithAgeRating(AgeRating.Everyone) .Build() @@ -104,7 +104,7 @@ public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDb .WithSeries(new SeriesBuilder("Test") .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1") .WithAgeRating(AgeRating.Everyone) .Build() @@ -164,7 +164,7 @@ public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDb .WithMetadata(new SeriesMetadataBuilder().Build()) .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1") .WithAgeRating(AgeRating.Everyone) .Build() @@ -227,7 +227,7 @@ public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDb .WithMetadata(new SeriesMetadataBuilder().Build()) .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1") .WithAgeRating(AgeRating.Everyone) .Build() @@ -307,7 +307,7 @@ public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDb .WithMetadata(new SeriesMetadataBuilder().Build()) .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1") .WithAgeRating(AgeRating.Everyone) .Build() @@ -366,7 +366,7 @@ public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDb .WithMetadata(new SeriesMetadataBuilder().Build()) .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1") .WithAgeRating(AgeRating.Everyone) .Build() @@ -433,7 +433,7 @@ public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDb .WithMetadata(new SeriesMetadataBuilder().Build()) .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1") .Build() ) @@ -475,7 +475,7 @@ public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDb .WithMetadata(new SeriesMetadataBuilder().Build()) .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1") .Build() ) @@ -620,7 +620,7 @@ public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDb .WithMetadata(new SeriesMetadataBuilder().Build()) .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1") .Build() ) @@ -674,7 +674,7 @@ public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDb .WithMetadata(new SeriesMetadataBuilder().Build()) .WithVolumes(new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1") .WithReleaseDate(new DateTime(2005, 03, 01)) .Build() @@ -769,8 +769,8 @@ public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDb } private static ReadingListItemDto CreateListItemDto(MangaFormat seriesFormat, LibraryType libraryType, - string volumeNumber = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, - string chapterNumber =API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, + string volumeNumber = Parser.LooseLeafVolume, + string chapterNumber =Parser.DefaultChapter, string chapterTitleName = "") { return new ReadingListItemDto() @@ -1082,7 +1082,7 @@ public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDb private static CblReadingList LoadCblFromPath(string path) { - var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ReadingListService/"); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ReadingListService/"); var reader = new System.Xml.Serialization.XmlSerializer(typeof(CblReadingList)); using var file = new StreamReader(Path.Join(testDirectory, path)); @@ -1368,7 +1368,7 @@ public class ReadingListServiceTests(ITestOutputHelper outputHelper): AbstractDb var series2 = new SeriesBuilder("Series 2") .WithFormat(MangaFormat.Archive) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) diff --git a/API.Tests/Services/ReadingProfileServiceTest.cs b/Kavita.Services.Tests/ReadingProfileServiceTest.cs similarity index 99% rename from API.Tests/Services/ReadingProfileServiceTest.cs rename to Kavita.Services.Tests/ReadingProfileServiceTest.cs index 44f034bd1..52ff9a468 100644 --- a/API.Tests/Services/ReadingProfileServiceTest.cs +++ b/Kavita.Services.Tests/ReadingProfileServiceTest.cs @@ -1,21 +1,22 @@ -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.Builders; -using API.Services; -using API.Tests.Helpers; using AutoMapper; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Common.Tests.Helpers; +using Kavita.Database; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.DTOs; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Services.Reading; using Microsoft.EntityFrameworkCore; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class ReadingProfileServiceTest(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { diff --git a/API.Tests/Services/ScannerServiceTests.cs b/Kavita.Services.Tests/ScannerServiceTests.cs similarity index 98% rename from API.Tests/Services/ScannerServiceTests.cs rename to Kavita.Services.Tests/ScannerServiceTests.cs index 0930a0690..97c6ef4f3 100644 --- a/API.Tests/Services/ScannerServiceTests.cs +++ b/Kavita.Services.Tests/ScannerServiceTests.cs @@ -1,26 +1,21 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Metadata; -using API.Data.Repositories; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Services.Tasks.Scanner.Parser; -using API.Tests.Helpers; -using Hangfire; -using Xunit; +using Hangfire; +using Kavita.API.Repositories; +using Kavita.Common.Extensions; +using Kavita.Database; +using Kavita.Database.Tests; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Services.Scanner; +using Kavita.Services.Tests.Helpers; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class ScannerServiceTests: AbstractDbTest { private readonly ITestOutputHelper _testOutputHelper; - private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/ScanTests"); + private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ScannerService/ScanTests"); public ScannerServiceTests(ITestOutputHelper testOutputHelper): base(testOutputHelper) { @@ -589,7 +584,7 @@ public class ScannerServiceTests: AbstractDbTest var testDirectoryPath = Path.Join( - Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/ScanTests"), + Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ScannerService/ScanTests"), testcase.Replace(".json", string.Empty)); library.Folders = [ @@ -645,7 +640,7 @@ public class ScannerServiceTests: AbstractDbTest var testDirectoryPath = Path.Join( - Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/ScanTests"), + Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/ScannerService/ScanTests"), testcase.Replace(".json", string.Empty)); library.Folders = [ @@ -701,7 +696,7 @@ public class ScannerServiceTests: AbstractDbTest var library = await scannerHelper.GenerateScannerData(testcase, infos); var testDirectoryPath = Path.Combine(Directory.GetCurrentDirectory(), - "../../../Services/Test Data/ScannerService/ScanTests", + "../../../Test Data/ScannerService/ScanTests", testcase.Replace(".json", string.Empty)); library.Folders = @@ -791,7 +786,7 @@ public class ScannerServiceTests: AbstractDbTest var library = await scannerHelper.GenerateScannerData(testcase, infos); var testDirectoryPath = Path.Combine(Directory.GetCurrentDirectory(), - "../../../Services/Test Data/ScannerService/ScanTests", + "../../../Test Data/ScannerService/ScanTests", testcase.Replace(".json", string.Empty)); library.Folders = diff --git a/API.Tests/Services/ScrobblingServiceTests.cs b/Kavita.Services.Tests/ScrobblingServiceTests.cs similarity index 96% rename from API.Tests/Services/ScrobblingServiceTests.cs rename to Kavita.Services.Tests/ScrobblingServiceTests.cs index 87aa1ad0f..aa33c5ed0 100644 --- a/API.Tests/Services/ScrobblingServiceTests.cs +++ b/Kavita.Services.Tests/ScrobblingServiceTests.cs @@ -1,24 +1,25 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Scrobbling; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Scrobble; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; -using API.Services.Reading; -using API.SignalR; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Database; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Scrobble; +using Kavita.Services.Builders; +using Kavita.Services.Plus; +using Kavita.Services.Reading; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; #nullable enable public class ScrobblingServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) @@ -305,7 +306,7 @@ public class ScrobblingServiceTests(ITestOutputHelper outputHelper): AbstractDbT Arg.Is(data => data.ChapterNumber == (int)chapter.MaxNumber && data.VolumeNumber == (int)volume.MaxNumber - ), + ), Arg.Any()); } @@ -629,13 +630,13 @@ public class ScrobblingServiceTests(ITestOutputHelper outputHelper): AbstractDbT [InlineData("https://anilist.co/manga/30105/Kekkaishi/", 30105)] public void CanParseWeblink_AniList(string link, int? expectedId) { - Assert.Equal(ScrobblingService.ExtractId(link, ScrobblingService.AniListWeblinkWebsite), expectedId); + Assert.Equal(ScrobblingHelper.ExtractId(link, ScrobblingService.AniListWeblinkWebsite), expectedId); } [Theory] [InlineData("https://mangadex.org/title/316d3d09-bb83-49da-9d90-11dc7ce40967/honzuki-no-gekokujou-shisho-ni-naru-tame-ni-wa-shudan-wo-erandeiraremasen-dai-3-bu-ryouchi-ni-hon-o", "316d3d09-bb83-49da-9d90-11dc7ce40967")] public void CanParseWeblink_MangaDex(string link, string expectedId) { - Assert.Equal(ScrobblingService.ExtractId(link, ScrobblingService.MangaDexWeblinkWebsite), expectedId); + Assert.Equal(ScrobblingHelper.ExtractId(link, ScrobblingService.MangaDexWeblinkWebsite), expectedId); } } diff --git a/API.Tests/Services/SeriesServiceTests.cs b/Kavita.Services.Tests/SeriesServiceTests.cs similarity index 99% rename from API.Tests/Services/SeriesServiceTests.cs rename to Kavita.Services.Tests/SeriesServiceTests.cs index 92af211d9..347208250 100644 --- a/API.Tests/Services/SeriesServiceTests.cs +++ b/Kavita.Services.Tests/SeriesServiceTests.cs @@ -1,34 +1,32 @@ -using System; -using System.Collections.Generic; -using System.Globalization; +using System.Globalization; using System.IO.Abstractions; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Metadata; -using API.DTOs.Person; -using API.DTOs.SeriesDetail; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Entities.Person; -using API.Extensions; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; +using Kavita.Common.Extensions; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.Person; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; +using Kavita.Services.Scanner; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; internal class MockHostingEnvironment : IHostEnvironment { public string ApplicationName { get => "API"; set => throw new NotImplementedException(); } @@ -53,7 +51,6 @@ public class SeriesServiceTests(ITestOutputHelper outputHelper): AbstractDbTest( { var ds = new DirectoryService(Substitute.For>(), new FileSystem()); - var locService = new LocalizationService(ds, new MockHostingEnvironment(), Substitute.For(), Substitute.For()); diff --git a/API.Tests/Services/SettingsServiceTests.cs b/Kavita.Services.Tests/SettingsServiceTests.cs similarity index 97% rename from API.Tests/Services/SettingsServiceTests.cs rename to Kavita.Services.Tests/SettingsServiceTests.cs index 6aac262e1..e6da9ea5a 100644 --- a/API.Tests/Services/SettingsServiceTests.cs +++ b/Kavita.Services.Tests/SettingsServiceTests.cs @@ -1,20 +1,17 @@ -using System.Collections.Generic; -using System.IO.Abstractions; -using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.KavitaPlus.Metadata; -using API.Entities; -using API.Entities.Enums; -using API.Entities.MetadataMatching; -using API.Services; -using API.Services.Tasks.Scanner; +using System.IO.Abstractions; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Scanner; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.MetadataMatching; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class SettingsServiceTests { @@ -33,7 +30,8 @@ public class SettingsServiceTests _mockUnitOfWork = Substitute.For(); _settingsService = new SettingsService(_mockUnitOfWork, ds, Substitute.For(), Substitute.For(), - Substitute.For>(), Substitute.For()); + Substitute.For>(), Substitute.For(), + Substitute.For()); } #region ImportMetadataSettings diff --git a/API.Tests/Services/SiteThemeServiceTests.cs b/Kavita.Services.Tests/SiteThemeServiceTests.cs similarity index 94% rename from API.Tests/Services/SiteThemeServiceTests.cs rename to Kavita.Services.Tests/SiteThemeServiceTests.cs index 327d82060..da83700b3 100644 --- a/API.Tests/Services/SiteThemeServiceTests.cs +++ b/Kavita.Services.Tests/SiteThemeServiceTests.cs @@ -1,21 +1,17 @@ using System.IO.Abstractions.TestingHelpers; -using System.Linq; -using System.Threading.Tasks; -using API.Data; -using API.Entities; -using API.Entities.Enums.Theme; -using API.Extensions; -using API.Services; -using API.Services.Tasks; -using API.SignalR; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Database; +using Kavita.Database.Tests; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums.Theme; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class SiteThemeServiceTest(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) diff --git a/API.Tests/Services/TachiyomiServiceTests.cs b/Kavita.Services.Tests/TachiyomiServiceTests.cs similarity index 92% rename from API.Tests/Services/TachiyomiServiceTests.cs rename to Kavita.Services.Tests/TachiyomiServiceTests.cs index 768bcc012..13a26e4a9 100644 --- a/API.Tests/Services/TachiyomiServiceTests.cs +++ b/Kavita.Services.Tests/TachiyomiServiceTests.cs @@ -1,22 +1,24 @@ -using API.Helpers.Builders; -using API.Services.Plus; -using API.Services.Reading; -using Xunit.Abstractions; - -namespace API.Tests.Services; -using System.Collections.Generic; -using System.IO.Abstractions.TestingHelpers; -using System.Threading.Tasks; -using Data; -using Data.Repositories; -using API.Entities; -using API.Entities.Enums; -using API.Services; -using SignalR; +using System.IO.Abstractions.TestingHelpers; using AutoMapper; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Services.Builders; +using Kavita.Services.Reading; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; +using Xunit.Abstractions; + +namespace Kavita.Services.Tests; public class TachiyomiServiceTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { @@ -45,7 +47,7 @@ public class TachiyomiServiceTests(ITestOutputHelper outputHelper): AbstractDbTe var (readerService, tachiyomiService) = Setup(unitOfWork, mapper); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .Build()) @@ -91,7 +93,7 @@ public class TachiyomiServiceTests(ITestOutputHelper outputHelper): AbstractDbTe var (readerService, tachiyomiService) = Setup(unitOfWork, mapper); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .Build()) @@ -143,7 +145,7 @@ public class TachiyomiServiceTests(ITestOutputHelper outputHelper): AbstractDbTe var (readerService, tachiyomiService) = Setup(unitOfWork, mapper); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .Build()) @@ -195,7 +197,7 @@ public class TachiyomiServiceTests(ITestOutputHelper outputHelper): AbstractDbTe var (readerService, tachiyomiService) = Setup(unitOfWork, mapper); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .Build()) @@ -248,15 +250,15 @@ public class TachiyomiServiceTests(ITestOutputHelper outputHelper): AbstractDbTe var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithPages(199).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithPages(192).Build()) .Build()) .WithVolume(new VolumeBuilder("3") - .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithChapter(new ChapterBuilder(Parser.DefaultChapter) .WithPages(255).Build()) .Build()) .WithPages(646) @@ -297,7 +299,7 @@ public class TachiyomiServiceTests(ITestOutputHelper outputHelper): AbstractDbTe var (readerService, tachiyomiService) = Setup(unitOfWork, mapper); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .Build()) @@ -351,7 +353,7 @@ public class TachiyomiServiceTests(ITestOutputHelper outputHelper): AbstractDbTe var (readerService, tachiyomiService) = Setup(unitOfWork, mapper); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .Build()) @@ -395,7 +397,7 @@ public class TachiyomiServiceTests(ITestOutputHelper outputHelper): AbstractDbTe var (readerService, tachiyomiService) = Setup(unitOfWork, mapper); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .Build()) @@ -446,7 +448,7 @@ public class TachiyomiServiceTests(ITestOutputHelper outputHelper): AbstractDbTe var (readerService, tachiyomiService) = Setup(unitOfWork, mapper); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .Build()) @@ -495,7 +497,7 @@ public class TachiyomiServiceTests(ITestOutputHelper outputHelper): AbstractDbTe var (unitOfWork, context, mapper) = await CreateDatabase(); var (readerService, tachiyomiService) = Setup(unitOfWork, mapper); var series = new SeriesBuilder("Test") - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .Build()) diff --git a/API.Tests/Services/Test Data/ArchiveService/Archives/LICENSE.md b/Kavita.Services.Tests/Test Data/ArchiveService/Archives/LICENSE.md similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/Archives/LICENSE.md rename to Kavita.Services.Tests/Test Data/ArchiveService/Archives/LICENSE.md diff --git a/API.Tests/Services/Test Data/ArchiveService/Archives/empty.zip b/Kavita.Services.Tests/Test Data/ArchiveService/Archives/empty.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/Archives/empty.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/Archives/empty.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/Archives/file in folder in folder.zip b/Kavita.Services.Tests/Test Data/ArchiveService/Archives/file in folder in folder.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/Archives/file in folder in folder.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/Archives/file in folder in folder.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/Archives/file in folder.zip b/Kavita.Services.Tests/Test Data/ArchiveService/Archives/file in folder.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/Archives/file in folder.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/Archives/file in folder.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/Archives/file in folder_alt.zip b/Kavita.Services.Tests/Test Data/ArchiveService/Archives/file in folder_alt.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/Archives/file in folder_alt.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/Archives/file in folder_alt.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/Archives/flat file.zip b/Kavita.Services.Tests/Test Data/ArchiveService/Archives/flat file.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/Archives/flat file.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/Archives/flat file.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/Archives/macos_native.zip b/Kavita.Services.Tests/Test Data/ArchiveService/Archives/macos_native.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/Archives/macos_native.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/Archives/macos_native.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/Archives/macos_none.zip b/Kavita.Services.Tests/Test Data/ArchiveService/Archives/macos_none.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/Archives/macos_none.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/Archives/macos_none.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/Archives/macos_one.zip b/Kavita.Services.Tests/Test Data/ArchiveService/Archives/macos_one.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/Archives/macos_one.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/Archives/macos_one.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/Archives/macos_withdotunder_one.zip b/Kavita.Services.Tests/Test Data/ArchiveService/Archives/macos_withdotunder_one.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/Archives/macos_withdotunder_one.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/Archives/macos_withdotunder_one.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/Archives/winrar.rar b/Kavita.Services.Tests/Test Data/ArchiveService/Archives/winrar.rar similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/Archives/winrar.rar rename to Kavita.Services.Tests/Test Data/ArchiveService/Archives/winrar.rar diff --git a/API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo.xml b/Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo.xml similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo.xml rename to Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo.xml diff --git a/API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo.zip b/Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo2.zip b/Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo2.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo2.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo2.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo_authors.zip b/Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo_authors.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo_authors.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo_authors.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo_duplicateInfos.rar b/Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo_duplicateInfos.rar similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo_duplicateInfos.rar rename to Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo_duplicateInfos.rar diff --git a/API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo_duplicateInfos.zip b/Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo_duplicateInfos.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo_duplicateInfos.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo_duplicateInfos.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo_duplicateInfos_reversed.zip b/Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo_duplicateInfos_reversed.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo_duplicateInfos_reversed.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo_duplicateInfos_reversed.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo_outside_root.zip b/Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo_outside_root.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo_outside_root.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo_outside_root.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo_outside_root_SharpCompress.cb7 b/Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo_outside_root_SharpCompress.cb7 similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/ComicInfos/ComicInfo_outside_root_SharpCompress.cb7 rename to Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/ComicInfo_outside_root_SharpCompress.cb7 diff --git a/API.Tests/Services/Test Data/ArchiveService/ComicInfos/Umlaut.zip b/Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/Umlaut.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/ComicInfos/Umlaut.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/Umlaut.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/ComicInfos/file in folder.zip b/Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/file in folder.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/ComicInfos/file in folder.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/ComicInfos/file in folder.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/macos_native.png b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/macos_native.png similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/macos_native.png rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/macos_native.png diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/macos_native.zip b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/macos_native.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/macos_native.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/macos_native.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/sorting.expected.png b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/sorting.expected.png similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/sorting.expected.png rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/sorting.expected.png diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/sorting.zip b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/sorting.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/sorting.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/sorting.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/test.expected.jpg b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/test.expected.jpg similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/test.expected.jpg rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/test.expected.jpg diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/test.zip b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/test.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/test.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/test.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/thumbnail.expected.jpg b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/thumbnail.expected.jpg similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/thumbnail.expected.jpg rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/thumbnail.expected.jpg diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/thumbnail.jpg b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/thumbnail.jpg similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/thumbnail.jpg rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/thumbnail.jpg diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - duplicate covers.cbz b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - duplicate covers.cbz similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - duplicate covers.cbz rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - duplicate covers.cbz diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - duplicate covers.expected.png b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - duplicate covers.expected.png similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - duplicate covers.expected.png rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - duplicate covers.expected.png diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - nested folder.cbz b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - nested folder.cbz similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - nested folder.cbz rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - nested folder.cbz diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - nested folder.expected.old.png b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - nested folder.expected.old.png similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - nested folder.expected.old.png rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - nested folder.expected.old.png diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - nested folder.expected.png b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - nested folder.expected.png similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - nested folder.expected.png rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - nested folder.expected.png diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - with folder.cbz b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - with folder.cbz similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - with folder.cbz rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - with folder.cbz diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - with folder.expected.jpg b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - with folder.expected.jpg similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - with folder.expected.jpg rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - with folder.expected.jpg diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - with folder.expected.png b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - with folder.expected.png similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - with folder.expected.png rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10 - with folder.expected.png diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/v10.cbz b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10.cbz similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/v10.cbz rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10.cbz diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/v10.expected.png b/Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10.expected.png similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/CoverImages/v10.expected.png rename to Kavita.Services.Tests/Test Data/ArchiveService/CoverImages/v10.expected.png diff --git a/API.Tests/Services/Test Data/ArchiveService/Formats/One File with DB_Supported.zip b/Kavita.Services.Tests/Test Data/ArchiveService/Formats/One File with DB_Supported.zip similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/Formats/One File with DB_Supported.zip rename to Kavita.Services.Tests/Test Data/ArchiveService/Formats/One File with DB_Supported.zip diff --git a/API.Tests/Services/Test Data/ArchiveService/Thumbnails/001.jpg b/Kavita.Services.Tests/Test Data/ArchiveService/Thumbnails/001.jpg similarity index 100% rename from API.Tests/Services/Test Data/ArchiveService/Thumbnails/001.jpg rename to Kavita.Services.Tests/Test Data/ArchiveService/Thumbnails/001.jpg diff --git a/API.Tests/Services/Test Data/BookService/Bizet-Variations_Chromatiques_de_concert_Theme_A4.pdf b/Kavita.Services.Tests/Test Data/BookService/Bizet-Variations_Chromatiques_de_concert_Theme_A4.pdf similarity index 100% rename from API.Tests/Services/Test Data/BookService/Bizet-Variations_Chromatiques_de_concert_Theme_A4.pdf rename to Kavita.Services.Tests/Test Data/BookService/Bizet-Variations_Chromatiques_de_concert_Theme_A4.pdf diff --git a/API.Tests/Services/Test Data/BookService/Relative Key Test File.epub b/Kavita.Services.Tests/Test Data/BookService/Relative Key Test File.epub similarity index 100% rename from API.Tests/Services/Test Data/BookService/Relative Key Test File.epub rename to Kavita.Services.Tests/Test Data/BookService/Relative Key Test File.epub diff --git a/API.Tests/Services/Test Data/BookService/Rollo at Work SP01.pdf b/Kavita.Services.Tests/Test Data/BookService/Rollo at Work SP01.pdf similarity index 100% rename from API.Tests/Services/Test Data/BookService/Rollo at Work SP01.pdf rename to Kavita.Services.Tests/Test Data/BookService/Rollo at Work SP01.pdf diff --git a/API.Tests/Services/Test Data/BookService/The Golden Harpoon; Or, Lost Among the Floes A Story of the Whaling Grounds.epub b/Kavita.Services.Tests/Test Data/BookService/The Golden Harpoon; Or, Lost Among the Floes A Story of the Whaling Grounds.epub similarity index 100% rename from API.Tests/Services/Test Data/BookService/The Golden Harpoon; Or, Lost Among the Floes A Story of the Whaling Grounds.epub rename to Kavita.Services.Tests/Test Data/BookService/The Golden Harpoon; Or, Lost Among the Floes A Story of the Whaling Grounds.epub diff --git a/API.Tests/Services/Test Data/BookService/TitleWithVolume.epub b/Kavita.Services.Tests/Test Data/BookService/TitleWithVolume.epub similarity index 100% rename from API.Tests/Services/Test Data/BookService/TitleWithVolume.epub rename to Kavita.Services.Tests/Test Data/BookService/TitleWithVolume.epub diff --git a/API.Tests/Services/Test Data/BookService/TitleWithVolume_NoSeriesOrSeriesIndex.epub b/Kavita.Services.Tests/Test Data/BookService/TitleWithVolume_NoSeriesOrSeriesIndex.epub similarity index 100% rename from API.Tests/Services/Test Data/BookService/TitleWithVolume_NoSeriesOrSeriesIndex.epub rename to Kavita.Services.Tests/Test Data/BookService/TitleWithVolume_NoSeriesOrSeriesIndex.epub diff --git a/API.Tests/Services/Test Data/BookService/content.opf b/Kavita.Services.Tests/Test Data/BookService/content.opf similarity index 100% rename from API.Tests/Services/Test Data/BookService/content.opf rename to Kavita.Services.Tests/Test Data/BookService/content.opf diff --git a/API.Tests/Services/Test Data/BookService/encrypted.pdf b/Kavita.Services.Tests/Test Data/BookService/encrypted.pdf similarity index 100% rename from API.Tests/Services/Test Data/BookService/encrypted.pdf rename to Kavita.Services.Tests/Test Data/BookService/encrypted.pdf diff --git a/API.Tests/Services/Test Data/BookService/indirect.pdf b/Kavita.Services.Tests/Test Data/BookService/indirect.pdf similarity index 100% rename from API.Tests/Services/Test Data/BookService/indirect.pdf rename to Kavita.Services.Tests/Test Data/BookService/indirect.pdf diff --git a/API.Tests/Services/Test Data/BookService/test.pdf b/Kavita.Services.Tests/Test Data/BookService/test.pdf similarity index 100% rename from API.Tests/Services/Test Data/BookService/test.pdf rename to Kavita.Services.Tests/Test Data/BookService/test.pdf diff --git a/API.Tests/Services/Test Data/BookService/test_ſ.pdf b/Kavita.Services.Tests/Test Data/BookService/test_ſ.pdf similarity index 100% rename from API.Tests/Services/Test Data/BookService/test_ſ.pdf rename to Kavita.Services.Tests/Test Data/BookService/test_ſ.pdf diff --git a/API.Tests/Services/Test Data/CacheService/Archives/file in folder in folder.zip b/Kavita.Services.Tests/Test Data/CacheService/Archives/file in folder in folder.zip similarity index 100% rename from API.Tests/Services/Test Data/CacheService/Archives/file in folder in folder.zip rename to Kavita.Services.Tests/Test Data/CacheService/Archives/file in folder in folder.zip diff --git a/API.Tests/Services/Test Data/CoverDbService/Existing/01.webp b/Kavita.Services.Tests/Test Data/CoverDbService/Existing/01.webp similarity index 100% rename from API.Tests/Services/Test Data/CoverDbService/Existing/01.webp rename to Kavita.Services.Tests/Test Data/CoverDbService/Existing/01.webp diff --git a/API.Tests/Services/Test Data/CoverDbService/Favicons/anilist.co.webp b/Kavita.Services.Tests/Test Data/CoverDbService/Favicons/anilist.co.webp similarity index 100% rename from API.Tests/Services/Test Data/CoverDbService/Favicons/anilist.co.webp rename to Kavita.Services.Tests/Test Data/CoverDbService/Favicons/anilist.co.webp diff --git a/API.Tests/Services/Test Data/DirectoryService/TestCases/Manga-testcase.txt b/Kavita.Services.Tests/Test Data/DirectoryService/TestCases/Manga-testcase.txt similarity index 100% rename from API.Tests/Services/Test Data/DirectoryService/TestCases/Manga-testcase.txt rename to Kavita.Services.Tests/Test Data/DirectoryService/TestCases/Manga-testcase.txt diff --git a/API.Tests/Services/Test Data/DirectoryService/extension/file.cbz b/Kavita.Services.Tests/Test Data/DirectoryService/extension/file.cbz similarity index 100% rename from API.Tests/Services/Test Data/DirectoryService/extension/file.cbz rename to Kavita.Services.Tests/Test Data/DirectoryService/extension/file.cbz diff --git a/API.Tests/Services/Test Data/DirectoryService/extension/file.rar b/Kavita.Services.Tests/Test Data/DirectoryService/extension/file.rar similarity index 100% rename from API.Tests/Services/Test Data/DirectoryService/extension/file.rar rename to Kavita.Services.Tests/Test Data/DirectoryService/extension/file.rar diff --git a/API.Tests/Services/Test Data/DirectoryService/extension/file2.cbz b/Kavita.Services.Tests/Test Data/DirectoryService/extension/file2.cbz similarity index 100% rename from API.Tests/Services/Test Data/DirectoryService/extension/file2.cbz rename to Kavita.Services.Tests/Test Data/DirectoryService/extension/file2.cbz diff --git a/API.Tests/Services/Test Data/DirectoryService/regex/file.txt b/Kavita.Services.Tests/Test Data/DirectoryService/regex/file.txt similarity index 100% rename from API.Tests/Services/Test Data/DirectoryService/regex/file.txt rename to Kavita.Services.Tests/Test Data/DirectoryService/regex/file.txt diff --git a/API.Tests/Services/Test Data/DirectoryService/regex/file2.txt b/Kavita.Services.Tests/Test Data/DirectoryService/regex/file2.txt similarity index 100% rename from API.Tests/Services/Test Data/DirectoryService/regex/file2.txt rename to Kavita.Services.Tests/Test Data/DirectoryService/regex/file2.txt diff --git a/API.Tests/Services/Test Data/ImageService/ColorScapes/blue-2.png b/Kavita.Services.Tests/Test Data/ImageService/ColorScapes/blue-2.png similarity index 100% rename from API.Tests/Services/Test Data/ImageService/ColorScapes/blue-2.png rename to Kavita.Services.Tests/Test Data/ImageService/ColorScapes/blue-2.png diff --git a/API.Tests/Services/Test Data/ImageService/ColorScapes/blue.jpg b/Kavita.Services.Tests/Test Data/ImageService/ColorScapes/blue.jpg similarity index 100% rename from API.Tests/Services/Test Data/ImageService/ColorScapes/blue.jpg rename to Kavita.Services.Tests/Test Data/ImageService/ColorScapes/blue.jpg diff --git a/API.Tests/Services/Test Data/ImageService/ColorScapes/green-red.png b/Kavita.Services.Tests/Test Data/ImageService/ColorScapes/green-red.png similarity index 100% rename from API.Tests/Services/Test Data/ImageService/ColorScapes/green-red.png rename to Kavita.Services.Tests/Test Data/ImageService/ColorScapes/green-red.png diff --git a/API.Tests/Services/Test Data/ImageService/ColorScapes/green.png b/Kavita.Services.Tests/Test Data/ImageService/ColorScapes/green.png similarity index 100% rename from API.Tests/Services/Test Data/ImageService/ColorScapes/green.png rename to Kavita.Services.Tests/Test Data/ImageService/ColorScapes/green.png diff --git a/API.Tests/Services/Test Data/ImageService/ColorScapes/lightblue-2.png b/Kavita.Services.Tests/Test Data/ImageService/ColorScapes/lightblue-2.png similarity index 100% rename from API.Tests/Services/Test Data/ImageService/ColorScapes/lightblue-2.png rename to Kavita.Services.Tests/Test Data/ImageService/ColorScapes/lightblue-2.png diff --git a/API.Tests/Services/Test Data/ImageService/ColorScapes/lightblue.png b/Kavita.Services.Tests/Test Data/ImageService/ColorScapes/lightblue.png similarity index 100% rename from API.Tests/Services/Test Data/ImageService/ColorScapes/lightblue.png rename to Kavita.Services.Tests/Test Data/ImageService/ColorScapes/lightblue.png diff --git a/API.Tests/Services/Test Data/ImageService/ColorScapes/pink.png b/Kavita.Services.Tests/Test Data/ImageService/ColorScapes/pink.png similarity index 100% rename from API.Tests/Services/Test Data/ImageService/ColorScapes/pink.png rename to Kavita.Services.Tests/Test Data/ImageService/ColorScapes/pink.png diff --git a/API.Tests/Services/Test Data/ImageService/ColorScapes/yellow-blue.png b/Kavita.Services.Tests/Test Data/ImageService/ColorScapes/yellow-blue.png similarity index 100% rename from API.Tests/Services/Test Data/ImageService/ColorScapes/yellow-blue.png rename to Kavita.Services.Tests/Test Data/ImageService/ColorScapes/yellow-blue.png diff --git a/API.Tests/Services/Test Data/ImageService/Covers/comic-normal-2.jpg b/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-normal-2.jpg similarity index 100% rename from API.Tests/Services/Test Data/ImageService/Covers/comic-normal-2.jpg rename to Kavita.Services.Tests/Test Data/ImageService/Covers/comic-normal-2.jpg diff --git a/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-normal-2_baseline.png b/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-normal-2_baseline.png new file mode 100644 index 000000000..f497577f6 Binary files /dev/null and b/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-normal-2_baseline.png differ diff --git a/API.Tests/Services/Test Data/ImageService/Covers/comic-normal-3.jpg b/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-normal-3.jpg similarity index 100% rename from API.Tests/Services/Test Data/ImageService/Covers/comic-normal-3.jpg rename to Kavita.Services.Tests/Test Data/ImageService/Covers/comic-normal-3.jpg diff --git a/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-normal-3_baseline.png b/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-normal-3_baseline.png new file mode 100644 index 000000000..9988185be Binary files /dev/null and b/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-normal-3_baseline.png differ diff --git a/API.Tests/Services/Test Data/ImageService/Covers/comic-normal.jpg b/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-normal.jpg similarity index 100% rename from API.Tests/Services/Test Data/ImageService/Covers/comic-normal.jpg rename to Kavita.Services.Tests/Test Data/ImageService/Covers/comic-normal.jpg diff --git a/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-normal_baseline.png b/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-normal_baseline.png new file mode 100644 index 000000000..f61fc34c9 Binary files /dev/null and b/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-normal_baseline.png differ diff --git a/API.Tests/Services/Test Data/ImageService/Covers/comic-square.jpg b/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-square.jpg similarity index 100% rename from API.Tests/Services/Test Data/ImageService/Covers/comic-square.jpg rename to Kavita.Services.Tests/Test Data/ImageService/Covers/comic-square.jpg diff --git a/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-square_baseline.png b/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-square_baseline.png new file mode 100644 index 000000000..bc8658bdf Binary files /dev/null and b/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-square_baseline.png differ diff --git a/API.Tests/Services/Test Data/ImageService/Covers/comic-wide.jpg b/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-wide.jpg similarity index 100% rename from API.Tests/Services/Test Data/ImageService/Covers/comic-wide.jpg rename to Kavita.Services.Tests/Test Data/ImageService/Covers/comic-wide.jpg diff --git a/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-wide_baseline.png b/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-wide_baseline.png new file mode 100644 index 000000000..61c996d09 Binary files /dev/null and b/Kavita.Services.Tests/Test Data/ImageService/Covers/comic-wide_baseline.png differ diff --git a/API.Tests/Services/Test Data/ImageService/Covers/manga-cover.png b/Kavita.Services.Tests/Test Data/ImageService/Covers/manga-cover.png similarity index 100% rename from API.Tests/Services/Test Data/ImageService/Covers/manga-cover.png rename to Kavita.Services.Tests/Test Data/ImageService/Covers/manga-cover.png diff --git a/Kavita.Services.Tests/Test Data/ImageService/Covers/manga-cover_baseline.png b/Kavita.Services.Tests/Test Data/ImageService/Covers/manga-cover_baseline.png new file mode 100644 index 000000000..b24421f4e Binary files /dev/null and b/Kavita.Services.Tests/Test Data/ImageService/Covers/manga-cover_baseline.png differ diff --git a/API.Tests/Services/Test Data/ImageService/Covers/spread-cover.jpg b/Kavita.Services.Tests/Test Data/ImageService/Covers/spread-cover.jpg similarity index 100% rename from API.Tests/Services/Test Data/ImageService/Covers/spread-cover.jpg rename to Kavita.Services.Tests/Test Data/ImageService/Covers/spread-cover.jpg diff --git a/Kavita.Services.Tests/Test Data/ImageService/Covers/spread-cover_baseline.png b/Kavita.Services.Tests/Test Data/ImageService/Covers/spread-cover_baseline.png new file mode 100644 index 000000000..bb12e6306 Binary files /dev/null and b/Kavita.Services.Tests/Test Data/ImageService/Covers/spread-cover_baseline.png differ diff --git a/API.Tests/Services/Test Data/ImageService/Covers/webtoon-strip-2.png b/Kavita.Services.Tests/Test Data/ImageService/Covers/webtoon-strip-2.png similarity index 100% rename from API.Tests/Services/Test Data/ImageService/Covers/webtoon-strip-2.png rename to Kavita.Services.Tests/Test Data/ImageService/Covers/webtoon-strip-2.png diff --git a/Kavita.Services.Tests/Test Data/ImageService/Covers/webtoon-strip-2_baseline.png b/Kavita.Services.Tests/Test Data/ImageService/Covers/webtoon-strip-2_baseline.png new file mode 100644 index 000000000..2338dd733 Binary files /dev/null and b/Kavita.Services.Tests/Test Data/ImageService/Covers/webtoon-strip-2_baseline.png differ diff --git a/API.Tests/Services/Test Data/ImageService/Covers/webtoon-strip.jpg b/Kavita.Services.Tests/Test Data/ImageService/Covers/webtoon-strip.jpg similarity index 100% rename from API.Tests/Services/Test Data/ImageService/Covers/webtoon-strip.jpg rename to Kavita.Services.Tests/Test Data/ImageService/Covers/webtoon-strip.jpg diff --git a/Kavita.Services.Tests/Test Data/ImageService/Covers/webtoon-strip_baseline.png b/Kavita.Services.Tests/Test Data/ImageService/Covers/webtoon-strip_baseline.png new file mode 100644 index 000000000..661f323b6 Binary files /dev/null and b/Kavita.Services.Tests/Test Data/ImageService/Covers/webtoon-strip_baseline.png differ diff --git a/API.Tests/Services/Test Data/ImageService/Covers/wide-ad.png b/Kavita.Services.Tests/Test Data/ImageService/Covers/wide-ad.png similarity index 100% rename from API.Tests/Services/Test Data/ImageService/Covers/wide-ad.png rename to Kavita.Services.Tests/Test Data/ImageService/Covers/wide-ad.png diff --git a/Kavita.Services.Tests/Test Data/ImageService/Covers/wide-ad_baseline.png b/Kavita.Services.Tests/Test Data/ImageService/Covers/wide-ad_baseline.png new file mode 100644 index 000000000..6c2664f9b Binary files /dev/null and b/Kavita.Services.Tests/Test Data/ImageService/Covers/wide-ad_baseline.png differ diff --git a/API.Tests/Services/Test Data/ImageService/cover.expected.jpg b/Kavita.Services.Tests/Test Data/ImageService/cover.expected.jpg similarity index 100% rename from API.Tests/Services/Test Data/ImageService/cover.expected.jpg rename to Kavita.Services.Tests/Test Data/ImageService/cover.expected.jpg diff --git a/API.Tests/Services/Test Data/OpdsService/test.zip b/Kavita.Services.Tests/Test Data/OpdsService/test.zip similarity index 100% rename from API.Tests/Services/Test Data/OpdsService/test.zip rename to Kavita.Services.Tests/Test Data/OpdsService/test.zip diff --git a/API.Tests/Services/Test Data/ReadingListService/Annual.cbl b/Kavita.Services.Tests/Test Data/ReadingListService/Annual.cbl similarity index 100% rename from API.Tests/Services/Test Data/ReadingListService/Annual.cbl rename to Kavita.Services.Tests/Test Data/ReadingListService/Annual.cbl diff --git a/API.Tests/Services/Test Data/ReadingListService/Fables.cbl b/Kavita.Services.Tests/Test Data/ReadingListService/Fables.cbl similarity index 100% rename from API.Tests/Services/Test Data/ReadingListService/Fables.cbl rename to Kavita.Services.Tests/Test Data/ReadingListService/Fables.cbl diff --git a/API.Tests/Services/Test Data/ScannerService/1x1.png b/Kavita.Services.Tests/Test Data/ScannerService/1x1.png similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/1x1.png rename to Kavita.Services.Tests/Test Data/ScannerService/1x1.png diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Alternating Removal - Manga.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Alternating Removal - Manga.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Alternating Removal - Manga.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Alternating Removal - Manga.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Base.zip b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Base.zip similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Base.zip rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Base.zip diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Delete Series In UI - Manga.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Delete Series In UI - Manga.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Delete Series In UI - Manga.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Delete Series In UI - Manga.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Exclude Pattern 1 - Manga.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Exclude Pattern 1 - Manga.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Exclude Pattern 1 - Manga.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Exclude Pattern 1 - Manga.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Flat Series - Manga.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Flat Series - Manga.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Flat Series - Manga.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Flat Series - Manga.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Flat Series with Specials Folder - Manga.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Flat Series with Specials Folder - Manga.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Flat Series with Specials Folder - Manga.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Flat Series with Specials Folder - Manga.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Flat Series with Specials Folder Alt Naming - Manga.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Flat Series with Specials Folder Alt Naming - Manga.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Flat Series with Specials Folder Alt Naming - Manga.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Flat Series with Specials Folder Alt Naming - Manga.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Flat Special - Manga.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Flat Special - Manga.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Flat Special - Manga.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Flat Special - Manga.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Image Series with SP Folder (Non English) - Image.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Image Series with SP Folder (Non English) - Image.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Image Series with SP Folder (Non English) - Image.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Image Series with SP Folder (Non English) - Image.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Image Series with SP Folder - Manga.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Image Series with SP Folder - Manga.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Image Series with SP Folder - Manga.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Image Series with SP Folder - Manga.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Localized Name matches Filename - Manga.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Localized Name matches Filename - Manga.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Localized Name matches Filename - Manga.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Localized Name matches Filename - Manga.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Manga-testcase.txt b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Manga-testcase.txt similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Manga-testcase.txt rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Manga-testcase.txt diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Multiple Roots - Manga.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Multiple Roots - Manga.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Multiple Roots - Manga.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Multiple Roots - Manga.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Nested Chapters - Manga.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Nested Chapters - Manga.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Nested Chapters - Manga.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Nested Chapters - Manga.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/PDF Comic Chapters - Comic.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/PDF Comic Chapters - Comic.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/PDF Comic Chapters - Comic.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/PDF Comic Chapters - Comic.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/PDF Comic Chapters - LightNovel.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/PDF Comic Chapters - LightNovel.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/PDF Comic Chapters - LightNovel.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/PDF Comic Chapters - LightNovel.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Publisher - ComicVine.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Publisher - ComicVine.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Publisher - ComicVine.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Publisher - ComicVine.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Scan Library Parses as ( - Manga.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Scan Library Parses as ( - Manga.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Scan Library Parses as ( - Manga.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Scan Library Parses as ( - Manga.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Series and Series-Series Combined - Manga.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Series and Series-Series Combined - Manga.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Series and Series-Series Combined - Manga.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Series and Series-Series Combined - Manga.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Series removed when no other changes are made - Manga.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Series removed when no other changes are made - Manga.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Series removed when no other changes are made - Manga.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Series removed when no other changes are made - Manga.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Extra - Manga.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Series with Extra - Manga.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Series with Extra - Manga.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Series with Extra - Manga.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Localized - Manga.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Series with Localized - Manga.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Series with Localized - Manga.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Series with Localized - Manga.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Localized 2 - Manga.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Series with Localized 2 - Manga.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Series with Localized 2 - Manga.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Series with Localized 2 - Manga.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Localized No Metadata - Manga.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Series with Localized No Metadata - Manga.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Series with Localized No Metadata - Manga.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Series with Localized No Metadata - Manga.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Series with Prefix - Book.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Series with Prefix - Book.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Series with Prefix - Book.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Series with Prefix - Book.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Series with slight differences No Metadata - Manga.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Series with slight differences No Metadata - Manga.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Series with slight differences No Metadata - Manga.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Series with slight differences No Metadata - Manga.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Sort Order - Manga.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Sort Order - Manga.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Sort Order - Manga.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Sort Order - Manga.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Subfolder always scanning fix publisher layout - Comic.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Subfolder always scanning fix publisher layout - Comic.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Subfolder always scanning fix publisher layout - Comic.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Subfolder always scanning fix publisher layout - Comic.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Subfolders always scanning all series changes - Manga.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Subfolders always scanning all series changes - Manga.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Subfolders always scanning all series changes - Manga.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Subfolders always scanning all series changes - Manga.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Subfolders and files at root (2) - Manga.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Subfolders and files at root (2) - Manga.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Subfolders and files at root (2) - Manga.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Subfolders and files at root (2) - Manga.json diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Subfolders and files at root - Manga.json b/Kavita.Services.Tests/Test Data/ScannerService/TestCases/Subfolders and files at root - Manga.json similarity index 100% rename from API.Tests/Services/Test Data/ScannerService/TestCases/Subfolders and files at root - Manga.json rename to Kavita.Services.Tests/Test Data/ScannerService/TestCases/Subfolders and files at root - Manga.json diff --git a/API.Tests/Services/TokenServiceTests.cs b/Kavita.Services.Tests/TokenServiceTests.cs similarity index 93% rename from API.Tests/Services/TokenServiceTests.cs rename to Kavita.Services.Tests/TokenServiceTests.cs index d40be5bc9..4844400bb 100644 --- a/API.Tests/Services/TokenServiceTests.cs +++ b/Kavita.Services.Tests/TokenServiceTests.cs @@ -1,7 +1,4 @@ -using API.Services; -using Xunit; - -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class TokenServiceTests { diff --git a/API.Tests/Services/VersionUpdaterServiceTests.cs b/Kavita.Services.Tests/VersionUpdaterServiceTests.cs similarity index 98% rename from API.Tests/Services/VersionUpdaterServiceTests.cs rename to Kavita.Services.Tests/VersionUpdaterServiceTests.cs index 8be8f4aee..567ac7a04 100644 --- a/API.Tests/Services/VersionUpdaterServiceTests.cs +++ b/Kavita.Services.Tests/VersionUpdaterServiceTests.cs @@ -1,18 +1,14 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using API.DTOs.Update; -using API.Services; -using API.Services.Tasks; -using API.SignalR; +using System.Collections; using Flurl.Http.Testing; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; using Kavita.Common.EnvironmentInfo; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.DTOs.Update; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class VersionUpdaterServiceTests : IDisposable { @@ -299,7 +295,7 @@ public class VersionUpdaterServiceTests : IDisposable var result = await _service.GetAllReleases(); - Assert.Single(result); + Assert.Single((IEnumerable)result); Assert.Equal("0.7.0.0", result[0].UpdateVersion); Assert.NotEmpty(_httpTest.CallLog); // HTTP call was made } diff --git a/API.Tests/Services/WordCountAnalysisTests.cs b/Kavita.Services.Tests/WordCountAnalysisTests.cs similarity index 89% rename from API.Tests/Services/WordCountAnalysisTests.cs rename to Kavita.Services.Tests/WordCountAnalysisTests.cs index 548995e1e..7fdee2782 100644 --- a/API.Tests/Services/WordCountAnalysisTests.cs +++ b/Kavita.Services.Tests/WordCountAnalysisTests.cs @@ -1,28 +1,28 @@ -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions.TestingHelpers; -using System.Threading.Tasks; -using API.Data; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers; -using API.Helpers.Builders; -using API.Services; -using API.Services.Plus; -using API.Services.Reading; -using API.Services.Tasks.Metadata; -using API.SignalR; +using System.IO.Abstractions.TestingHelpers; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; +using Kavita.Common.Extensions; +using Kavita.Database.Tests; +using Kavita.Models.Builders; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Builders; +using Kavita.Services.Helpers; +using Kavita.Services.Metadata; +using Kavita.Services.Reading; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NSubstitute; -using Xunit; using Xunit.Abstractions; -namespace API.Tests.Services; +namespace Kavita.Services.Tests; public class WordCountAnalysisTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper) { - private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService"); + private readonly string _testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Test Data/BookService"); private const long WordCount = 33608; // 37417 if splitting on space, 33608 if just character count private const long MinHoursToRead = 1; private const float AvgHoursToRead = 1.66954792f; @@ -61,7 +61,7 @@ public class WordCountAnalysisTests(ITestOutputHelper outputHelper): AbstractDbT series.Volumes = new List() { - new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(chapter) .Build(), }; @@ -110,7 +110,7 @@ public class WordCountAnalysisTests(ITestOutputHelper outputHelper): AbstractDbT .Build(); var series = new SeriesBuilder("Test Series") .WithFormat(MangaFormat.Epub) - .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) + .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) .WithChapter(chapter) .Build()) .Build(); diff --git a/Kavita.Services/AccountService.cs b/Kavita.Services/AccountService.cs new file mode 100644 index 000000000..af9f0d468 --- /dev/null +++ b/Kavita.Services/AccountService.cs @@ -0,0 +1,268 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using Kavita.API.Database; +using Kavita.API.Errors; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.Common; +using Kavita.Models; +using Kavita.Models.Builders; +using Kavita.Models.Constants; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Models.Extensions; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Kavita.Services; + +public partial class AccountService( + UserManager userManager, + ILogger logger, + IUnitOfWork unitOfWork, + IMapper mapper, + ILocalizationService localizationService) + : IAccountService +{ + public const string DefaultPassword = "[k.2@RZ!mxCQkJzE"; + private static readonly Regex AllowedUsernameRegex = AllowedUsernameRegexAttr(); + + + public async Task> ChangeUserPassword(AppUser user, string newPassword, CancellationToken ct = default) + { + var passwordValidationIssues = (await ValidatePassword(user, newPassword, ct)).ToList(); + if (passwordValidationIssues.Count != 0) return passwordValidationIssues; + + var result = await userManager.RemovePasswordAsync(user); + if (!result.Succeeded) + { + logger.LogError("Could not update password"); + return result.Errors.Select(e => new ApiException(400, e.Code, e.Description)); + } + + result = await userManager.AddPasswordAsync(user, newPassword); + if (result.Succeeded) return []; + + logger.LogError("Could not update password"); + return result.Errors.Select(e => new ApiException(400, e.Code, e.Description)); + } + + public async Task> ValidatePassword(AppUser user, string password, CancellationToken ct = default) + { + foreach (var validator in userManager.PasswordValidators) + { + var validationResult = await validator.ValidateAsync(userManager, user, password); + if (!validationResult.Succeeded) + { + return validationResult.Errors.Select(e => new ApiException(400, e.Code, e.Description)); + } + } + + return []; + } + public async Task> ValidateUsername(string? username, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(username) || !AllowedUsernameRegex.IsMatch(username)) + { + return [new ApiException(400, "Invalid username")]; + } + + // Reverted because of https://go.microsoft.com/fwlink/?linkid=2129535 + if (await userManager.Users.AnyAsync(x => x.NormalizedUserName != null + && x.NormalizedUserName == username.ToUpper(), ct)) + { + return + [ + new(400, "Username is already taken") + ]; + } + + return []; + } + + public async Task> ValidateEmail(string email, CancellationToken ct = default) + { + var user = await unitOfWork.UserRepository.GetUserByEmailAsync(email, ct: ct); + if (user == null) return []; + + return + [ + new ApiException(400, "Email is already registered") + ]; + } + + /// + /// Does the user have Change Restriction permission or admin rights and not Read Only + /// + /// + /// + /// + public async Task CanChangeAgeRestriction(AppUser? user, CancellationToken ct = default) + { + if (user == null) return false; + + var roles = await userManager.GetRolesAsync(user); + if (roles.Contains(PolicyConstants.ReadOnlyRole)) return false; + + return roles.Contains(PolicyConstants.ChangeRestrictionRole) || roles.Contains(PolicyConstants.AdminRole); + } + + public async Task ChangeIdentityProvider(int actingUserId, AppUser user, IdentityProvider identityProvider, + CancellationToken ct = default) + { + var defaultAdminUser = await unitOfWork.UserRepository.GetDefaultAdminUser(ct: ct); + if (user.Id == defaultAdminUser.Id) + { + if (identityProvider == IdentityProvider.OpenIdConnect) + { + throw new KavitaException(await localizationService.Translate(actingUserId, "cannot-change-identity-provider-original-user")); + } + + return false; + } + + // Allow changes if users aren't being synced + var oidcSettings = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct)).OidcConfig; + if (!oidcSettings.SyncUserSettings) + { + user.IdentityProvider = identityProvider; + await unitOfWork.CommitAsync(ct); + return false; + } + + // Don't allow changes to the user if they're managed by oidc, and their identity provider isn't being changed to something else + if (user.IdentityProvider == IdentityProvider.OpenIdConnect && identityProvider == IdentityProvider.OpenIdConnect) + { + throw new KavitaException(await localizationService.Translate(actingUserId, "oidc-managed")); + } + + user.IdentityProvider = identityProvider; + await unitOfWork.CommitAsync(ct); + + return user.IdentityProvider == IdentityProvider.OpenIdConnect; + } + + public async Task UpdateLibrariesForUser(AppUser user, IList librariesIds, bool hasAdminRole, CancellationToken ct = default) + { + var allLibraries = (await unitOfWork.LibraryRepository.GetLibrariesAsync(LibraryIncludes.AppUser, ct: ct)).ToList(); + var currentLibrary = allLibraries.Where(l => l.AppUsers.Contains(user)).ToList(); + + List libraries; + if (hasAdminRole) + { + logger.LogDebug("{UserId} is admin. Granting access to all libraries", user.Id); + libraries = allLibraries; + } + else + { + libraries = allLibraries.Where(lib => librariesIds.Contains(lib.Id)).ToList(); + } + + var toRemove = currentLibrary.Except(libraries); + var toAdd = libraries.Except(currentLibrary); + + foreach (var lib in toRemove) + { + lib.AppUsers ??= []; + lib.AppUsers.Remove(user); + user.RemoveSideNavFromLibrary(lib); + } + + foreach (var lib in toAdd) + { + lib.AppUsers ??= []; + lib.AppUsers.Add(user); + user.CreateSideNavFromLibrary(lib); + } + } + + public async Task> UpdateRolesForUser(AppUser user, IList roles, + CancellationToken ct = default) + { + var existingRoles = await userManager.GetRolesAsync(user); + var hasAdminRole = roles.Contains(PolicyConstants.AdminRole); + if (!hasAdminRole) + { + roles.Add(PolicyConstants.PlebRole); + } + + if (existingRoles.Except(roles).Any() || roles.Except(existingRoles).Any()) + { + var roleResult = await userManager.RemoveFromRolesAsync(user, existingRoles); + if (!roleResult.Succeeded) return roleResult.Errors; + + roleResult = await userManager.AddToRolesAsync(user, roles); + if (!roleResult.Succeeded) return roleResult.Errors; + } + + return []; + } + + public async Task SeedUser(AppUser user, CancellationToken ct = default) + { + AddDefaultStreamsToUser(user, ct); + AddDefaultHighlightSlotsToUser(user); + AddAuthKeys(user); + await AddDefaultReadingProfileToUser(user, ct); // Commits + } + + /// + /// Assign default streams + /// + /// + /// + public void AddDefaultStreamsToUser(AppUser user, CancellationToken ct = default) + { + foreach (var newStream in Defaults.DefaultStreams.Select(mapper.Map)) + { + user.DashboardStreams.Add(newStream); + } + + foreach (var stream in Defaults.DefaultSideNavStreams.Select(mapper.Map)) + { + user.SideNavStreams.Add(stream); + } + } + + private void AddDefaultHighlightSlotsToUser(AppUser user) + { + if (user.UserPreferences.BookReaderHighlightSlots.Any()) return; + + user.UserPreferences.BookReaderHighlightSlots = Defaults.DefaultHighlightSlots.ToList(); + unitOfWork.UserRepository.Update(user); + } + + private void AddAuthKeys(AppUser user) + { + if (user.AuthKeys.Any()) return; + + user.AuthKeys = Defaults.CreateDefaultAuthKeys(); + unitOfWork.UserRepository.Update(user); + } + + /// + /// Assign default reading profile + /// + /// + /// + public async Task AddDefaultReadingProfileToUser(AppUser user, CancellationToken ct = default) + { + var profile = new AppUserReadingProfileBuilder(user.Id) + .WithName("Default Profile") + .WithKind(ReadingProfileKind.Default) + .Build(); + + unitOfWork.AppUserReadingProfileRepository.Add(profile); + + await unitOfWork.CommitAsync(ct); + } + + [GeneratedRegex(@"^[a-zA-Z0-9\-._@+/]*$")] + private static partial Regex AllowedUsernameRegexAttr(); +} diff --git a/API/Services/AnnotationService.cs b/Kavita.Services/AnnotationService.cs similarity index 89% rename from API/Services/AnnotationService.cs rename to Kavita.Services/AnnotationService.cs index db279f074..f5293d86a 100644 --- a/API/Services/AnnotationService.cs +++ b/Kavita.Services/AnnotationService.cs @@ -1,35 +1,24 @@ -#nullable enable -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text.Encodings.Web; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Annotations; -using API.DTOs.Reader; -using API.Entities; -using API.Helpers; -using API.SignalR; using HtmlAgilityPack; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Models.DTOs.Annotations; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities.User; +using Kavita.Services.Helpers; using Microsoft.Extensions.Logging; -namespace API.Services; - -public interface IAnnotationService -{ - Task CreateAnnotation(int userId, AnnotationDto dto); - Task UpdateAnnotation(int userId, AnnotationDto dto); - /// - /// Export all annotations for a user, or optionally specify which annotation exactly - /// - /// - /// - /// - Task ExportAnnotations(int userId, IList? annotationIds = null); -} +namespace Kavita.Services; public class AnnotationService( ILogger logger, @@ -51,9 +40,10 @@ public class AnnotationService( /// /// /// + /// /// /// Message is not localized - public async Task CreateAnnotation(int userId, AnnotationDto dto) + public async Task CreateAnnotation(int userId, AnnotationDto dto, CancellationToken ct = default) { try { @@ -62,7 +52,7 @@ public class AnnotationService( throw new KavitaException("invalid-payload"); } - var chapter = await unitOfWork.ChapterRepository.GetChapterAsync(dto.ChapterId) ?? throw new KavitaException("chapter-doesnt-exist"); + var chapter = await unitOfWork.ChapterRepository.GetChapterAsync(dto.ChapterId, ct: ct) ?? throw new KavitaException("chapter-doesnt-exist"); var chapterTitle = string.Empty; try @@ -101,9 +91,9 @@ public class AnnotationService( }; unitOfWork.AnnotationRepository.Attach(annotation); - await unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); - return (await unitOfWork.AnnotationRepository.GetAnnotationDto(annotation.Id))!; + return (await unitOfWork.AnnotationRepository.GetAnnotationDto(annotation.Id, ct))!; } catch (Exception ex) { @@ -117,13 +107,14 @@ public class AnnotationService( /// /// /// + /// /// /// Message is not localized - public async Task UpdateAnnotation(int userId, AnnotationDto dto) + public async Task UpdateAnnotation(int userId, AnnotationDto dto, CancellationToken ct = default) { try { - var annotation = await unitOfWork.AnnotationRepository.GetAnnotation(dto.Id); + var annotation = await unitOfWork.AnnotationRepository.GetAnnotation(dto.Id, ct); if (annotation == null || annotation.AppUserId != userId) throw new KavitaException("denied"); annotation.ContainsSpoiler = dto.ContainsSpoiler; @@ -134,12 +125,13 @@ public class AnnotationService( unitOfWork.AnnotationRepository.Update(annotation); - if (!unitOfWork.HasChanges() || await unitOfWork.CommitAsync()) + if (!unitOfWork.HasChanges() || await unitOfWork.CommitAsync(ct)) { - dto = (await unitOfWork.AnnotationRepository.GetAnnotationDto(annotation.Id))!; + dto = (await unitOfWork.AnnotationRepository.GetAnnotationDto(annotation.Id, ct))!; await eventHub.SendMessageToAsync(MessageFactory.AnnotationUpdate, MessageFactory.AnnotationUpdateEvent(dto), userId); + return dto; } } catch (Exception ex) @@ -150,7 +142,8 @@ public class AnnotationService( throw new KavitaException("generic-error"); } - public async Task ExportAnnotations(int userId, IList? annotationIds = null) + public async Task ExportAnnotations(int userId, IList? annotationIds = null, + CancellationToken ct = default) { try { @@ -158,11 +151,11 @@ public class AnnotationService( IList annotations; if (annotationIds == null) { - annotations = await unitOfWork.AnnotationRepository.GetFullAnnotationsByUserIdAsync(userId); + annotations = await unitOfWork.AnnotationRepository.GetFullAnnotationsByUserIdAsync(userId, ct); } else { - annotations = await unitOfWork.AnnotationRepository.GetFullAnnotations(userId, annotationIds); + annotations = await unitOfWork.AnnotationRepository.GetFullAnnotations(userId, annotationIds, ct); } var userIds = annotations.Select(a => a.UserId) @@ -170,12 +163,12 @@ public class AnnotationService( .ToList(); // Get users with preferences for highlight colors - var users = (await unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.UserPreferences, false)) + var users = (await unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.UserPreferences, false, ct)) .Where(u => userIds.Contains(u.Id)) .ToDictionary(u => u.Id, u => u); // Get settings for hostname - var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct); var hostname = !string.IsNullOrWhiteSpace(settings.HostName) ? settings.HostName : $"http://localhost:{Configuration.Port}"; // Group annotations by series, then by volume diff --git a/API/Services/ArchiveService.cs b/Kavita.Services/ArchiveService.cs similarity index 63% rename from API/Services/ArchiveService.cs rename to Kavita.Services/ArchiveService.cs index 4285769bf..db1bbff0b 100644 --- a/API/Services/ArchiveService.cs +++ b/Kavita.Services/ArchiveService.cs @@ -7,68 +7,33 @@ using System.Linq; using System.Threading.Tasks; using System.Xml.Linq; using System.Xml.Serialization; -using API.Data.Metadata; -using API.DTOs.Archive; -using API.Entities.Enums; -using API.Extensions; -using API.Services.Tasks; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs.Archive; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Services.Extensions; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using SharpCompress.Archives; using SharpCompress.Common; -namespace API.Services; - -#nullable enable - -public interface IArchiveService -{ - void ExtractArchive(string archivePath, string extractPath); - int GetNumberOfPagesFromArchive(string archivePath); - string GetCoverImage(string archivePath, string fileName, string outputDirectory, EncodeFormat format, CoverImageSize size = CoverImageSize.Default); - bool IsValidArchive(string archivePath); - ComicInfo? GetComicInfo(string archivePath); - ArchiveLibrary CanOpen(string archivePath); - bool ArchiveNeedsFlattening(ZipArchive archive); - /// - /// 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); -} +namespace Kavita.Services; /// /// Responsible for manipulating Archive files. Used by and /// // ReSharper disable once ClassWithVirtualMembersNeverInherited.Global -public class ArchiveService : IArchiveService +public class ArchiveService( + ILogger logger, + IDirectoryService directoryService, + IImageService imageService, + IMediaErrorService mediaErrorService) + : IArchiveService { - private readonly ILogger _logger; - private readonly IDirectoryService _directoryService; - private readonly IImageService _imageService; - private readonly IMediaErrorService _mediaErrorService; private const string ComicInfoFilename = "ComicInfo.xml"; - public ArchiveService(ILogger logger, IDirectoryService directoryService, - IImageService imageService, IMediaErrorService mediaErrorService) - { - _logger = logger; - _directoryService = directoryService; - _imageService = imageService; - _mediaErrorService = mediaErrorService; - } - /// /// Checks if a File can be opened. Requires up to 2 opens of the filestream. /// @@ -76,9 +41,9 @@ public class ArchiveService : IArchiveService /// public virtual ArchiveLibrary CanOpen(string archivePath) { - if (string.IsNullOrEmpty(archivePath) || !(File.Exists(archivePath) && Tasks.Scanner.Parser.Parser.IsArchive(archivePath) || Tasks.Scanner.Parser.Parser.IsEpub(archivePath))) return ArchiveLibrary.NotSupported; + if (string.IsNullOrEmpty(archivePath) || !(File.Exists(archivePath) && Parser.IsArchive(archivePath) || Parser.IsEpub(archivePath))) return ArchiveLibrary.NotSupported; - var ext = _directoryService.FileSystem.Path.GetExtension(archivePath).ToUpper(); + var ext = directoryService.FileSystem.Path.GetExtension(archivePath).ToUpper(); if (ext.Equals(".CBR") || ext.Equals(".RAR")) return ArchiveLibrary.SharpCompress; try @@ -90,7 +55,7 @@ public class ArchiveService : IArchiveService { try { - using var a1 = ArchiveFactory.Open(archivePath); + using var a1 = ArchiveFactory.OpenArchive(archivePath); return ArchiveLibrary.SharpCompress; } catch (Exception) @@ -104,7 +69,7 @@ public class ArchiveService : IArchiveService { if (!IsValidArchive(archivePath)) { - _logger.LogError("Archive {ArchivePath} could not be found", archivePath); + logger.LogError("Archive {ArchivePath} could not be found", archivePath); return 0; } @@ -116,29 +81,29 @@ public class ArchiveService : IArchiveService case ArchiveLibrary.Default: { using var archive = ZipFile.OpenRead(archivePath); - return archive.Entries.Count(e => !Tasks.Scanner.Parser.Parser.HasBlacklistedFolderInPath(e.FullName) && Tasks.Scanner.Parser.Parser.IsImage(e.FullName)); + return archive.Entries.Count(e => !Parser.HasBlacklistedFolderInPath(e.FullName) && Parser.IsImage(e.FullName)); } case ArchiveLibrary.SharpCompress: { - using var archive = ArchiveFactory.Open(archivePath); + using var archive = ArchiveFactory.OpenArchive(archivePath); return archive.Entries.Count(entry => !entry.IsDirectory && - !Tasks.Scanner.Parser.Parser.HasBlacklistedFolderInPath(Path.GetDirectoryName(entry.Key) ?? string.Empty) - && Tasks.Scanner.Parser.Parser.IsImage(entry.Key)); + !Parser.HasBlacklistedFolderInPath(Path.GetDirectoryName(entry.Key) ?? string.Empty) + && Parser.IsImage(entry.Key)); } case ArchiveLibrary.NotSupported: - _logger.LogWarning("[GetNumberOfPagesFromArchive] This archive cannot be read: {ArchivePath}. Defaulting to 0 pages", archivePath); - _mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService, "File format not supported", string.Empty); + logger.LogWarning("[GetNumberOfPagesFromArchive] This archive cannot be read: {ArchivePath}. Defaulting to 0 pages", archivePath); + mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService, "File format not supported", string.Empty); return 0; default: - _logger.LogWarning("[GetNumberOfPagesFromArchive] There was an exception when reading archive stream: {ArchivePath}. Defaulting to 0 pages", archivePath); - _mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService, "File format not supported", string.Empty); + logger.LogWarning("[GetNumberOfPagesFromArchive] There was an exception when reading archive stream: {ArchivePath}. Defaulting to 0 pages", archivePath); + mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService, "File format not supported", string.Empty); return 0; } } catch (Exception ex) { - _logger.LogWarning(ex, "[GetNumberOfPagesFromArchive] There was an exception when reading archive stream: {ArchivePath}. Defaulting to 0 pages", archivePath); - _mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService, + logger.LogWarning(ex, "[GetNumberOfPagesFromArchive] There was an exception when reading archive stream: {ArchivePath}. Defaulting to 0 pages", archivePath); + mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService, "This archive cannot be read or not supported", ex); return 0; } @@ -152,9 +117,9 @@ public class ArchiveService : IArchiveService public static string? FindFolderEntry(IEnumerable entryFullNames) { var result = entryFullNames - .Where(path => !(Path.EndsInDirectorySeparator(path) || Tasks.Scanner.Parser.Parser.HasBlacklistedFolderInPath(path) || path.StartsWith(Tasks.Scanner.Parser.Parser.MacOsMetadataFileStartsWith))) + .Where(path => !(Path.EndsInDirectorySeparator(path) || Parser.HasBlacklistedFolderInPath(path) || path.StartsWith(Parser.MacOsMetadataFileStartsWith))) .OrderByNatural(Path.GetFileNameWithoutExtension) - .FirstOrDefault(Tasks.Scanner.Parser.Parser.IsCoverImage); + .FirstOrDefault(Parser.IsCoverImage); return string.IsNullOrEmpty(result) ? null : result; } @@ -170,7 +135,7 @@ public class ArchiveService : IArchiveService // First check if there are any files that are not in a nested folder before just comparing by filename. This is needed // because NaturalSortComparer does not work with paths and doesn't seem 001.jpg as before chapter 1/001.jpg. var fullNames = entryFullNames - .Where(path => !(Path.EndsInDirectorySeparator(path) || Tasks.Scanner.Parser.Parser.HasBlacklistedFolderInPath(path) || path.StartsWith(Tasks.Scanner.Parser.Parser.MacOsMetadataFileStartsWith)) && Tasks.Scanner.Parser.Parser.IsImage(path)) + .Where(path => !(Path.EndsInDirectorySeparator(path) || Parser.HasBlacklistedFolderInPath(path) || path.StartsWith(Parser.MacOsMetadataFileStartsWith)) && Parser.IsImage(path)) .OrderByNatural(c => c.GetFullPathWithoutExtension()) .ToList(); if (fullNames.Count == 0) return null; @@ -207,7 +172,7 @@ public class ArchiveService : IArchiveService /// /// Generates byte array of cover image. - /// Given a path to a compressed file , will ensure the first image (respects directory structure) is returned unless + /// Given a path to a compressed file , will ensure the first image (respects directory structure) is returned unless /// a folder/cover.(image extension) exists in the the compressed file (if duplicate, the first is chosen) /// /// This skips over any __MACOSX folder/file iteration. @@ -234,11 +199,11 @@ public class ArchiveService : IArchiveService var entry = archive.Entries.Single(e => e.FullName == entryName); using var stream = entry.Open(); - return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, format, size); + return imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, format, size); } case ArchiveLibrary.SharpCompress: { - using var archive = ArchiveFactory.Open(archivePath); + using var archive = ArchiveFactory.OpenArchive(archivePath); var entryNames = archive.Entries.Where(archiveEntry => !archiveEntry.IsDirectory).Select(e => e.Key).ToList(); var entryName = FindCoverImageFilename(archivePath, entryNames); @@ -246,21 +211,21 @@ public class ArchiveService : IArchiveService var entry = archive.Entries.Single(e => e.Key == entryName); using var stream = entry.OpenEntryStream(); - return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, format, size); + return imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, format, size); } case ArchiveLibrary.NotSupported: - _logger.LogWarning("[GetCoverImage] This archive cannot be read: {ArchivePath}. Defaulting to no cover image", archivePath); + logger.LogWarning("[GetCoverImage] This archive cannot be read: {ArchivePath}. Defaulting to no cover image", archivePath); return string.Empty; default: - _logger.LogWarning("[GetCoverImage] There was an exception when reading archive stream: {ArchivePath}. Defaulting to no cover image", archivePath); + logger.LogWarning("[GetCoverImage] There was an exception when reading archive stream: {ArchivePath}. Defaulting to no cover image", archivePath); return string.Empty; } } catch (Exception ex) { - _logger.LogWarning(ex, "[GetCoverImage] There was an exception when reading archive stream: {ArchivePath}. Defaulting to no cover image", archivePath); - _mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService, - "This archive cannot be read or not supported", ex); // TODO: Localize this + logger.LogWarning(ex, "[GetCoverImage] There was an exception when reading archive stream: {ArchivePath}. Defaulting to no cover image", archivePath); + mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService, + "This archive cannot be read or not supported", ex); // TODO: Localize this. Which user? } return string.Empty; @@ -289,7 +254,7 @@ public class ArchiveService : IArchiveService // Sometimes ZipArchive will list the directory and others it will just keep it in the FullName return archive.Entries.Count > 0 && !Path.HasExtension(archive.Entries[0].FullName) || - archive.Entries.Any(e => e.FullName.Contains(Path.AltDirectorySeparatorChar) && !Tasks.Scanner.Parser.Parser.HasBlacklistedFolderInPath(e.FullName)); + archive.Entries.Any(e => e.FullName.Contains(Path.AltDirectorySeparatorChar) && !Parser.HasBlacklistedFolderInPath(e.FullName)); } /// @@ -303,31 +268,31 @@ public class ArchiveService : IArchiveService { var dateString = DateTime.UtcNow.ToShortDateString().Replace("/", "_"); - var tempLocation = Path.Join(_directoryService.TempDirectory, $"{tempFolder}_{dateString}"); - var potentialExistingFile = _directoryService.FileSystem.FileInfo.New(Path.Join(_directoryService.TempDirectory, $"kavita_{tempFolder}_{dateString}.zip")); + var tempLocation = Path.Join(directoryService.TempDirectory, $"{tempFolder}_{dateString}"); + var potentialExistingFile = directoryService.FileSystem.FileInfo.New(Path.Join(directoryService.TempDirectory, $"kavita_{tempFolder}_{dateString}.zip")); if (potentialExistingFile.Exists) { // A previous download exists, just return it immediately return potentialExistingFile.FullName; } - _directoryService.ExistOrCreate(tempLocation); + directoryService.ExistOrCreate(tempLocation); - if (!_directoryService.CopyFilesToDirectory(files, tempLocation)) + if (!directoryService.CopyFilesToDirectory(files, tempLocation)) { throw new KavitaException("bad-copy-files-for-download"); } - var zipPath = Path.Join(_directoryService.TempDirectory, $"kavita_{tempFolder}_{dateString}.zip"); + var zipPath = Path.Join(directoryService.TempDirectory, $"kavita_{tempFolder}_{dateString}.zip"); try { ZipFile.CreateFromDirectory(tempLocation, zipPath); // Remove the folder as we have the zip - _directoryService.ClearAndDeleteDirectory(tempLocation); + directoryService.ClearAndDeleteDirectory(tempLocation); } catch (AggregateException ex) { - _logger.LogError(ex, "There was an issue creating temp archive"); + logger.LogError(ex, "There was an issue creating temp archive"); throw new KavitaException("generic-create-temp-archive"); } @@ -338,7 +303,7 @@ public class ArchiveService : IArchiveService { var dateString = DateTime.UtcNow.ToShortDateString().Replace("/", "_"); - var potentialExistingFile = _directoryService.FileSystem.FileInfo.New(Path.Join(_directoryService.TempDirectory, $"kavita_{tempFolder}_{dateString}.cbz")); + 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 @@ -346,32 +311,32 @@ public class ArchiveService : IArchiveService } // Extract all the files to a temp directory and create zip on that - var tempLocation = Path.Join(_directoryService.TempDirectory, $"{tempFolder}_{dateString}"); + var tempLocation = Path.Join(directoryService.TempDirectory, $"{tempFolder}_{dateString}"); var totalFiles = files.Count + 1; var count = 1f; try { - _directoryService.ExistOrCreate(tempLocation); + directoryService.ExistOrCreate(tempLocation); foreach (var path in files) { - var tempPath = Path.Join(tempLocation, _directoryService.FileSystem.Path.GetFileNameWithoutExtension(_directoryService.FileSystem.FileInfo.New(path).Name)); + var tempPath = Path.Join(tempLocation, directoryService.FileSystem.Path.GetFileNameWithoutExtension(directoryService.FileSystem.FileInfo.New(path).Name)); // Image series need different handling - if (Tasks.Scanner.Parser.Parser.IsImage(path)) + if (Parser.IsImage(path)) { - var parentDirectory = _directoryService.FileSystem.DirectoryInfo.New(path).Parent?.Name; - tempPath = Path.Join(tempLocation, parentDirectory ?? _directoryService.FileSystem.FileInfo.New(path).Name); + var parentDirectory = directoryService.FileSystem.DirectoryInfo.New(path).Parent?.Name; + tempPath = Path.Join(tempLocation, parentDirectory ?? directoryService.FileSystem.FileInfo.New(path).Name); } - if (Tasks.Scanner.Parser.Parser.IsArchive(path)) + if (Parser.IsArchive(path)) { // Archives don't need to be put into a subdirectory of the same name - tempPath = _directoryService.GetParentDirectoryName(tempPath); + tempPath = directoryService.GetParentDirectoryName(tempPath); } - progressCallback(Tuple.Create(_directoryService.FileSystem.FileInfo.New(path).Name, (1.0f * totalFiles) / count)); + progressCallback(Tuple.Create(directoryService.FileSystem.FileInfo.New(path).Name, (1.0f * totalFiles) / count)); - _directoryService.CopyFileToDirectory(path, tempPath); + directoryService.CopyFileToDirectory(path, tempPath); count++; } } @@ -380,16 +345,16 @@ public class ArchiveService : IArchiveService throw new KavitaException("bad-copy-files-for-download"); } - var zipPath = Path.Join(_directoryService.TempDirectory, $"kavita_{tempFolder}_{dateString}.cbz"); + 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); + directoryService.ClearAndDeleteDirectory(tempLocation); } catch (AggregateException ex) { - _logger.LogError(ex, "There was an issue creating temp archive"); + logger.LogError(ex, "There was an issue creating temp archive"); throw new KavitaException("generic-create-temp-archive"); } @@ -406,22 +371,22 @@ public class ArchiveService : IArchiveService { if (!File.Exists(archivePath)) { - _logger.LogWarning("Archive {ArchivePath} could not be found", archivePath); + logger.LogWarning("Archive {ArchivePath} could not be found", archivePath); return false; } - if (Tasks.Scanner.Parser.Parser.IsArchive(archivePath)) return true; + if (Parser.IsArchive(archivePath)) return true; - _logger.LogWarning("Archive {ArchivePath} is not a valid archive", archivePath); + logger.LogWarning("Archive {ArchivePath} is not a valid archive", archivePath); return false; } private static bool IsComicInfoArchiveEntry(string? fullName, string name) { if (fullName == null) return false; - return !Tasks.Scanner.Parser.Parser.HasBlacklistedFolderInPath(fullName) + return !Parser.HasBlacklistedFolderInPath(fullName) && name.EndsWith(ComicInfoFilename, StringComparison.OrdinalIgnoreCase) - && !name.StartsWith(Tasks.Scanner.Parser.Parser.MacOsMetadataFileStartsWith); + && !name.StartsWith(Parser.MacOsMetadataFileStartsWith); } /// @@ -456,7 +421,7 @@ public class ArchiveService : IArchiveService } case ArchiveLibrary.SharpCompress: { - using var archive = ArchiveFactory.Open(archivePath); + using var archive = ArchiveFactory.OpenArchive(archivePath); var entry = archive.Entries.FirstOrDefault(entry => entry.Key == ComicInfoFilename) ?? archive.Entries.FirstOrDefault(entry => IsComicInfoArchiveEntry(Path.GetDirectoryName(entry.Key), entry.Key)); @@ -471,10 +436,10 @@ public class ArchiveService : IArchiveService break; } case ArchiveLibrary.NotSupported: - _logger.LogWarning("[GetComicInfo] This archive cannot be read: {ArchivePath}", archivePath); + logger.LogWarning("[GetComicInfo] This archive cannot be read: {ArchivePath}", archivePath); return null; default: - _logger.LogWarning( + logger.LogWarning( "[GetComicInfo] There was an exception when reading archive stream: {ArchivePath}", archivePath); return null; @@ -482,8 +447,8 @@ public class ArchiveService : IArchiveService } catch (Exception ex) { - _logger.LogWarning(ex, "[GetComicInfo] There was an exception when reading archive stream: {Filepath}", archivePath); - _mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService, + logger.LogWarning(ex, "[GetComicInfo] There was an exception when reading archive stream: {Filepath}", archivePath); + mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService, "This archive cannot be read or not supported", ex); } @@ -507,7 +472,9 @@ public class ArchiveService : IArchiveService if (reader == null) return null; var info = (ComicInfo?) serializer.Deserialize(reader); - ComicInfo.CleanComicInfo(info); + + info.CleanComicInfo(); + return info; } @@ -515,7 +482,7 @@ public class ArchiveService : IArchiveService private void ExtractArchiveEntities(IEnumerable entries, string extractPath) { - _directoryService.ExistOrCreate(extractPath); + directoryService.ExistOrCreate(extractPath); // TODO: Look into a Parallel.ForEach foreach (var entry in entries) { @@ -535,8 +502,8 @@ public class ArchiveService : IArchiveService archive.ExtractToDirectory(extractPath, true); if (!needsFlattening) return; - _logger.LogDebug("Extracted archive is nested in root folder, flattening..."); - _directoryService.Flatten(extractPath); + logger.LogDebug("Extracted archive is nested in root folder, flattening..."); + directoryService.Flatten(extractPath); } /// @@ -551,11 +518,11 @@ public class ArchiveService : IArchiveService { if (!IsValidArchive(archivePath)) return; - if (_directoryService.FileSystem.Directory.Exists(extractPath)) return; + if (directoryService.FileSystem.Directory.Exists(extractPath)) return; - if (!_directoryService.FileSystem.File.Exists(archivePath)) + if (!directoryService.FileSystem.File.Exists(archivePath)) { - _logger.LogError("{Archive} does not exist on disk", archivePath); + logger.LogError("{Archive} does not exist on disk", archivePath); throw new KavitaException($"{archivePath} does not exist on disk"); } @@ -574,29 +541,29 @@ public class ArchiveService : IArchiveService } case ArchiveLibrary.SharpCompress: { - using var archive = ArchiveFactory.Open(archivePath); + using var archive = ArchiveFactory.OpenArchive(archivePath); ExtractArchiveEntities(archive.Entries.Where(entry => !entry.IsDirectory - && !Tasks.Scanner.Parser.Parser.HasBlacklistedFolderInPath(Path.GetDirectoryName(entry.Key) ?? string.Empty) - && Tasks.Scanner.Parser.Parser.IsImage(entry.Key)), extractPath); + && !Parser.HasBlacklistedFolderInPath(Path.GetDirectoryName(entry.Key) ?? string.Empty) + && Parser.IsImage(entry.Key)), extractPath); break; } case ArchiveLibrary.NotSupported: - _logger.LogWarning("[ExtractArchive] This archive cannot be read: {ArchivePath}", archivePath); + logger.LogWarning("[ExtractArchive] This archive cannot be read: {ArchivePath}", archivePath); return; default: - _logger.LogWarning("[ExtractArchive] There was an exception when reading archive stream: {ArchivePath}", archivePath); + logger.LogWarning("[ExtractArchive] There was an exception when reading archive stream: {ArchivePath}", archivePath); return; } } catch (Exception ex) { - _logger.LogWarning(ex, "[ExtractArchive] There was a problem extracting {ArchivePath} to {ExtractPath}",archivePath, extractPath); - _mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService, + logger.LogWarning(ex, "[ExtractArchive] There was a problem extracting {ArchivePath} to {ExtractPath}",archivePath, extractPath); + mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService, "This archive cannot be read or not supported", ex); throw new KavitaException( $"There was an error when extracting {archivePath}. Check the file exists, has read permissions or the server OS can support all path characters."); } - _logger.LogDebug("Extracted archive to {ExtractPath} in {ElapsedMilliseconds} milliseconds", extractPath, sw.ElapsedMilliseconds); + logger.LogDebug("Extracted archive to {ExtractPath} in {ElapsedMilliseconds} milliseconds", extractPath, sw.ElapsedMilliseconds); } } diff --git a/Kavita.Services/AuthKeyService.cs b/Kavita.Services/AuthKeyService.cs new file mode 100644 index 000000000..8df2b1d1d --- /dev/null +++ b/Kavita.Services/AuthKeyService.cs @@ -0,0 +1,34 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Kavita.API.Database; +using Kavita.API.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Hybrid; +using Microsoft.Extensions.Logging; + +namespace Kavita.Services; + +public class AuthKeyService(IDataContext context, ILogger logger, HybridCache cache) : IAuthKeyService +{ + public async Task UpdateLastAccessedAsync(string authKey, CancellationToken ct = default) + { + logger.LogTrace("Updating last accessed Auth key: {AuthKey}", authKey); + await context.AppUserAuthKey + .Where(k => k.Key == authKey) + .ExecuteUpdateAsync(s => + s.SetProperty(k => k.LastAccessedAtUtc, DateTime.UtcNow), cancellationToken: ct); + } + + public async Task InvalidateAsync(string keyValue, CancellationToken cancellationToken = default) + { + var cacheKey = CreateCacheKey(keyValue); + await cache.RemoveAsync(cacheKey, cancellationToken); + } + + public string CreateCacheKey(string keyValue) + { + return $"authKey_{keyValue}"; + } +} diff --git a/Kavita.Services/BackupService.cs b/Kavita.Services/BackupService.cs new file mode 100644 index 000000000..3f531b55c --- /dev/null +++ b/Kavita.Services/BackupService.cs @@ -0,0 +1,292 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; +using Kavita.Common.EnvironmentInfo; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities.Enums; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Kavita.Services; + +public class BackupService( + ILogger logger, + IUnitOfWork unitOfWork, + IDirectoryService directoryService, + IEventHub eventHub) + : IBackupService +{ + private readonly IList _backupFiles = + [ + "appsettings.json" + ]; + + /// + /// Returns a list of all log files for Kavita + /// + /// If file rolling is enabled. Defaults to True. + /// + public IEnumerable GetLogFiles(bool rollFiles = true) + { + var multipleFileRegex = rollFiles ? @"\d*" : string.Empty; + var fi = directoryService.FileSystem.FileInfo.New(IBackupService.LogFile); + + var files = rollFiles + ? directoryService.GetFiles(directoryService.LogDirectory, + $@"{directoryService.FileSystem.Path.GetFileNameWithoutExtension(fi.Name)}{multipleFileRegex}\.log") + : [directoryService.FileSystem.Path.Join(directoryService.LogDirectory, "kavita.log")]; + return files; + } + + /// + /// Will back up anything that needs to be backed up. This includes logs, setting files, bare minimum cover images (just locked and first cover). + /// + /// + [AutomaticRetry(Attempts = 3, LogEvents = false, OnAttemptsExceeded = AttemptsExceededAction.Fail)] + public async Task BackupDatabase(CancellationToken ct = default) + { + logger.LogInformation("Beginning backup of Database at {BackupTime}", DateTime.Now); + var backupDirectory = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BackupDirectory)).Value; + + logger.LogDebug("Backing up to {BackupDirectory}", backupDirectory); + if (!directoryService.ExistOrCreate(backupDirectory)) + { + logger.LogCritical("Could not write to {BackupDirectory}; aborting backup", backupDirectory); + await eventHub.SendMessageAsync(MessageFactory.Error, + MessageFactory.ErrorEvent("Backup Service Error",$"Could not write to {backupDirectory}; aborting backup"), ct: ct); + return; + } + + await SendProgress(0F, "Started backup", ct); + await SendProgress(0.1F, "Copying core files", ct); + + var dateString = $"{DateTime.UtcNow.ToShortDateString()}_{DateTime.UtcNow:s}Z".Replace("/", "_").Replace(":", "_"); + var zipPath = directoryService.FileSystem.Path.Join(backupDirectory, $"kavita_backup_v{BuildInfo.Version}_{dateString}.zip"); + + if (File.Exists(zipPath)) + { + logger.LogCritical("{ZipFile} already exists, aborting", zipPath); + await eventHub.SendMessageAsync(MessageFactory.Error, + MessageFactory.ErrorEvent("Backup Service Error",$"{zipPath} already exists, aborting"), ct: ct); + return; + } + + var tempDirectory = Path.Join(directoryService.TempDirectory, dateString); + directoryService.ExistOrCreate(tempDirectory); + directoryService.ClearDirectory(tempDirectory); + + await SendProgress(0.1F, "Backing up database", ct); + await BackupDatabaseFile(tempDirectory); + + await SendProgress(0.15F, "Copying config files", ct); + directoryService.CopyFilesToDirectory( + _backupFiles.Select(file => directoryService.FileSystem.Path.Join(directoryService.ConfigDirectory, file)), tempDirectory); + + // Copy any csv's as those are used for manual migrations + directoryService.CopyFilesToDirectory( + directoryService.GetFilesWithCertainExtensions(directoryService.ConfigDirectory, @"\.csv"), tempDirectory); + + await SendProgress(0.2F, "Copying logs", ct); + CopyLogsToBackupDirectory(tempDirectory); + + await SendProgress(0.25F, "Copying cover images", ct); + await CopyCoverImagesToBackupDirectory(tempDirectory); + + await SendProgress(0.35F, "Copying templates images", ct); + CopyTemplatesToBackupDirectory(tempDirectory); + + await SendProgress(0.5F, "Copying bookmarks", ct); + await CopyBookmarksToBackupDirectory(tempDirectory); + + await SendProgress(0.6F, "Copying Fonts", ct); + CopyFontsToBackupDirectory(tempDirectory); + + await SendProgress(0.75F, "Copying themes", ct); + CopyThemesToBackupDirectory(tempDirectory); + + await SendProgress(0.85F, "Copying favicons", ct); + CopyFaviconsToBackupDirectory(tempDirectory); + + try + { + await ZipFile.CreateFromDirectoryAsync(tempDirectory, zipPath); + } + catch (AggregateException ex) + { + logger.LogError(ex, "There was an issue when archiving library backup"); + } + + directoryService.ClearAndDeleteDirectory(tempDirectory); + logger.LogInformation("Database backup completed"); + await SendProgress(1F, "Completed backup", ct); + } + + private void CopyLogsToBackupDirectory(string tempDirectory) + { + var files = GetLogFiles(); + directoryService.CopyFilesToDirectory(files, directoryService.FileSystem.Path.Join(tempDirectory, "logs")); + } + + /// + /// Creates a backup of the SQLite database using VACUUM INTO command. + /// This method safely backs up the database while it's in use, without locking issues. + /// + /// The directory where the backup file will be created + private async Task BackupDatabaseFile(string tempDirectory) + { + var backupPath = directoryService.FileSystem.Path.Join(tempDirectory, "kavita.db"); + + // Validate the backup path to prevent SQL injection + // The path must not contain single quotes which could break the SQL command + if (backupPath.Contains('\'')) + { + throw new ArgumentException("Backup path contains invalid characters", nameof(tempDirectory)); + } + + try + { + // Use VACUUM INTO to create a safe backup of the database while it's running + // This creates a consistent snapshot without locking the main database + // Note: VACUUM INTO requires a literal path and cannot use SQL parameters + #pragma warning disable EF1002 // The backup path is validated above to not contain SQL injection characters + await unitOfWork.DataContext.Database.ExecuteSqlRawAsync($"VACUUM INTO '{backupPath}'"); + #pragma warning restore EF1002 + logger.LogDebug("Database backup created successfully at {BackupPath}", backupPath); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to create database backup using VACUUM INTO at {BackupPath}", backupPath); + throw new InvalidOperationException($"Failed to create database backup at {backupPath}", ex); + } + } + + private void CopyFaviconsToBackupDirectory(string tempDirectory) + { + directoryService.CopyDirectoryToDirectory(directoryService.FaviconDirectory, directoryService.FileSystem.Path.Join(tempDirectory, "favicons")); + } + + private void CopyTemplatesToBackupDirectory(string tempDirectory) + { + directoryService.CopyDirectoryToDirectory(directoryService.TemplateDirectory, directoryService.FileSystem.Path.Join(tempDirectory, "templates")); + } + + private async Task CopyCoverImagesToBackupDirectory(string tempDirectory) + { + var outputTempDir = Path.Join(tempDirectory, "covers"); + directoryService.ExistOrCreate(outputTempDir); + + try + { + var seriesImages = await unitOfWork.SeriesRepository.GetLockedCoverImagesAsync(); + directoryService.CopyFilesToDirectory( + seriesImages.Select(s => directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory, s)), outputTempDir); + + var collectionTags = await unitOfWork.CollectionTagRepository.GetAllCoverImagesAsync(); + directoryService.CopyFilesToDirectory( + collectionTags.Select(s => directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory, s)), outputTempDir); + + var chapterImages = await unitOfWork.ChapterRepository.GetCoverImagesForLockedChaptersAsync(); + directoryService.CopyFilesToDirectory( + chapterImages.Select(s => directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory, s)), outputTempDir); + + var volumeImages = await unitOfWork.VolumeRepository.GetCoverImagesForLockedVolumesAsync(); + directoryService.CopyFilesToDirectory( + volumeImages.Select(s => directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory, s)), outputTempDir); + + var libraryImages = await unitOfWork.LibraryRepository.GetAllCoverImagesAsync(); + directoryService.CopyFilesToDirectory( + libraryImages.Select(s => directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory, s)), outputTempDir); + + var readingListImages = await unitOfWork.ReadingListRepository.GetAllCoverImagesAsync(); + directoryService.CopyFilesToDirectory( + readingListImages.Select(s => directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory, s)), outputTempDir); + } + catch (IOException) + { + // Swallow exception. This can be a duplicate cover being copied as chapter and volumes can share same file. + } + + if (!directoryService.GetFiles(outputTempDir, searchOption: SearchOption.AllDirectories).Any()) + { + directoryService.ClearAndDeleteDirectory(outputTempDir); + } + } + + private async Task CopyBookmarksToBackupDirectory(string tempDirectory) + { + var bookmarkDirectory = + (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; + + var outputTempDir = Path.Join(tempDirectory, "bookmarks"); + directoryService.ExistOrCreate(outputTempDir); + + try + { + directoryService.CopyDirectoryToDirectory(bookmarkDirectory, outputTempDir); + } + catch (IOException) + { + // Swallow exception. + } + + if (!directoryService.GetFiles(outputTempDir, searchOption: SearchOption.AllDirectories).Any()) + { + directoryService.ClearAndDeleteDirectory(outputTempDir); + } + } + + private void CopyFontsToBackupDirectory(string tempDirectory) + { + var outputTempDir = Path.Join(tempDirectory, "fonts"); + directoryService.ExistOrCreate(outputTempDir); + + try + { + directoryService.CopyDirectoryToDirectory(directoryService.EpubFontDirectory, outputTempDir); + } + catch (IOException ex) + { + logger.LogWarning(ex, "Failed to copy fonts to backup directory '{OutputTempDir}'. Fonts will not be included in the backup.", outputTempDir); + } + + if (!directoryService.GetFiles(outputTempDir, searchOption: SearchOption.AllDirectories).Any()) + { + directoryService.ClearAndDeleteDirectory(outputTempDir); + } + } + + private void CopyThemesToBackupDirectory(string tempDirectory) + { + var outputTempDir = Path.Join(tempDirectory, "themes"); + directoryService.ExistOrCreate(outputTempDir); + + try + { + directoryService.CopyDirectoryToDirectory(directoryService.SiteThemeDirectory, outputTempDir); + } + catch (IOException) + { + // Swallow exception. + } + + if (!directoryService.GetFiles(outputTempDir, searchOption: SearchOption.AllDirectories).Any()) + { + directoryService.ClearAndDeleteDirectory(outputTempDir); + } + } + + private async Task SendProgress(float progress, string subtitle, CancellationToken ct = default) + { + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.BackupDatabaseProgressEvent(progress, subtitle), ct: ct); + } + +} diff --git a/API/Services/BookService.cs b/Kavita.Services/BookService.cs similarity index 88% rename from API/Services/BookService.cs rename to Kavita.Services/BookService.cs index 05c4c60cd..f0fae9493 100644 --- a/API/Services/BookService.cs +++ b/Kavita.Services/BookService.cs @@ -5,23 +5,29 @@ using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.Data.Metadata; -using API.DTOs.Reader; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Services.Tasks.Scanner.Parser; -using API.Helpers; -using API.Services.Tasks.Metadata; using Docnet.Core; using Docnet.Core.Converters; using Docnet.Core.Models; using Docnet.Core.Readers; using ExCSS; using HtmlAgilityPack; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.User; +using Kavita.Models.Metadata; +using Kavita.Models.Parser; +using Kavita.Services.Extensions; +using Kavita.Services.Helpers; +using Kavita.Services.Metadata; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using Microsoft.IO; using Nager.ArticleNumber; @@ -31,54 +37,23 @@ using VersOne.Epub; using VersOne.Epub.Options; using VersOne.Epub.Schema; -namespace API.Services; +namespace Kavita.Services; -#nullable enable - -public interface IBookService +public partial class BookService( + ILogger logger, + IDirectoryService directoryService, + IImageService imageService, + IMediaErrorService mediaErrorService, + IUnitOfWork unitOfWork) + : IBookService { - int GetNumberOfPages(string filePath); - string GetCoverImage(string fileFilePath, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); - ComicInfo? GetComicInfo(string filePath); - ParserInfo? ParseInfo(string filePath); - /// - /// Scopes styles to .reading-section and replaces img src to the passed apiBase - /// - /// - /// - /// If the stylesheetHtml contains Import statements, when scoping the filename, scope needs to be wrt filepath. - /// Book Reference, needed for if you expect Import statements - /// - Task ScopeStyles(string stylesheetHtml, string apiBase, string filename, EpubBookRef book); - /// - /// Extracts a PDF file's pages as images to a target directory - /// - /// This method relies on Docnet which has explicit patches from Kavita for ARM support. This should only be used with Tachiyomi - /// - /// Where the files will be extracted to. If doesn't exist, will be created. - void ExtractPdfImages(string fileFilePath, string targetDirectory); - Task> GenerateTableOfContents(Chapter chapter); - Task GetBookPage(int page, int chapterId, string cachedEpubPath, string baseUrl, List ptocBookmarks, List annotations); - Task> CreateKeyToPageMappingAsync(EpubBookRef book); - Task?> GetWordCountsPerPage(string bookFilePath); - Task GetWordCountBetweenXPaths(string bookFilePath, string startXpath, int startPage, string endXpath, int endPage); - Task CopyImageToTempFromBook(int chapterId, BookmarkDto bookmarkDto, string cachedBookPath); - Task GetResourceAsync(string bookFilePath, string requestedKey); -} - -public partial class BookService : IBookService -{ - private readonly ILogger _logger; - private readonly IDirectoryService _directoryService; - private readonly IImageService _imageService; - private readonly IMediaErrorService _mediaErrorService; private readonly StylesheetParser _cssParser = new (); private static readonly RecyclableMemoryStreamManager StreamManager = new (); private const string CssScopeClass = ".book-content"; - private const string BookApiUrl = "book-resources?file="; + private const string BookApiUrl = "book-resources?apiKey={0}&file="; public const string BookReaderBodyScope = "//BODY/APP-ROOT[1]/DIV[1]/DIV[1]/DIV[1]/APP-BOOK-READER[1]/DIV[1]/DIV[2]/DIV[1]/DIV[1]/DIV[1]"; - private readonly PdfComicInfoExtractor _pdfComicInfoExtractor; - private readonly IUnitOfWork _unitOfWork; + + private readonly PdfComicInfoExtractor _pdfComicInfoExtractor = new(logger, mediaErrorService); /// /// Setup the most lenient book parsing options possible as people have some really bad epubs @@ -125,16 +100,6 @@ public partial class BookService : IBookService } }; - public BookService(ILogger logger, IDirectoryService directoryService, IImageService imageService, IMediaErrorService mediaErrorService, IUnitOfWork unitOfWork) - { - _logger = logger; - _unitOfWork = unitOfWork; - _directoryService = directoryService; - _imageService = imageService; - _mediaErrorService = mediaErrorService; - _pdfComicInfoExtractor = new PdfComicInfoExtractor(_logger, _mediaErrorService); - } - private static bool HasClickableHrefPart(HtmlNode anchor) { return anchor.GetAttributeValue("href", string.Empty).Contains('#') @@ -215,8 +180,10 @@ public partial class BookService : IBookService /// /// If the stylesheetHtml contains Import statements, when scoping the filename, scope needs to be wrt filepath. /// Book Reference, needed for if you expect Import statements + /// /// - public async Task ScopeStyles(string stylesheetHtml, string apiBase, string filename, EpubBookRef book) + public async Task ScopeStyles(string stylesheetHtml, string apiBase, string filename, EpubBookRef book, + CancellationToken ct = default) { // @Import statements will be handled by browser, so we must inline the css into the original file that request it, so they can be Scoped var prepend = filename.Length > 0 ? filename.Replace(Path.GetFileName(filename), string.Empty) : string.Empty; @@ -232,7 +199,7 @@ public partial class BookService : IBookService { key = prepend + key; } - if (!book.Content.AllFiles.TryGetLocalFileRefByKey(key, out var bookFile)) continue; + if (!book.Content.AllFiles.TryGetLocalFileRefByKey(key, out var bookFile) || bookFile == null) continue; var content = await bookFile.ReadContentAsBytesAsync(); importBuilder.Append(Encoding.UTF8.GetString(content)); @@ -254,7 +221,7 @@ public partial class BookService : IBookService if (string.IsNullOrEmpty(styleContent)) return string.Empty; - var stylesheet = await _cssParser.ParseAsync(styleContent); + var stylesheet = await _cssParser.ParseAsync(styleContent, ct); foreach (var styleRule in stylesheet.StyleRules) { if (styleRule.Selector.Text == CssScopeClass) continue; @@ -274,8 +241,9 @@ public partial class BookService : IBookService } catch (Exception ex) { - _logger.LogError(ex, "There was an issue escaping css, likely due to an unsupported css rule"); + logger.LogError(ex, "There was an issue escaping css, likely due to an unsupported css rule"); } + return RemoveWhiteSpaceFromStylesheets($"{CssScopeClass} {styleContent}"); } @@ -340,7 +308,7 @@ public partial class BookService : IBookService } catch (Exception ex) { - _logger.LogWarning(ex, "Failed to inject a text (ptoc) bookmark into file"); + logger.LogWarning(ex, "Failed to inject a text (ptoc) bookmark into file"); // Swallow } } @@ -462,7 +430,7 @@ public partial class BookService : IBookService } } - private async Task InlineStyles(HtmlDocument doc, EpubBookRef book, string apiBase, HtmlNode body) + private async Task InlineStyles(HtmlDocument doc, EpubBookRef book, string apiBase, HtmlNode body, CancellationToken ct = default) { var inlineStyles = doc.DocumentNode.SelectNodes("//style"); // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract @@ -470,7 +438,7 @@ public partial class BookService : IBookService { foreach (var inlineStyle in inlineStyles) { - var styleContent = await ScopeStyles(inlineStyle.InnerHtml, apiBase, "", book); + var styleContent = await ScopeStyles(inlineStyle.InnerHtml, apiBase, "", book, ct); body.PrependChild(HtmlNode.CreateNode($"")); } } @@ -489,7 +457,7 @@ public partial class BookService : IBookService var correctedKey = book.Content.Css.Local.Select(s => s.Key).SingleOrDefault(s => s.EndsWith(key)); if (correctedKey == null) { - _logger.LogError("Epub is Malformed, key: {Key} is not matching OPF file", key); + logger.LogError("Epub is Malformed, key: {Key} is not matching OPF file", key); continue; } @@ -501,7 +469,7 @@ public partial class BookService : IBookService var cssFile = book.Content.Css.GetLocalFileRefByKey(key); var stylesheetHtml = await cssFile.ReadContentAsync(); - var styleContent = await ScopeStyles(stylesheetHtml, apiBase, cssFile.FilePath, book); + var styleContent = await ScopeStyles(stylesheetHtml, apiBase, cssFile.FilePath, book, ct); // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if (styleContent != null) { @@ -510,9 +478,9 @@ public partial class BookService : IBookService } catch (Exception ex) { - _logger.LogError(ex, "There was an error reading css file for inlining likely due to a key mismatch in metadata"); - await _mediaErrorService.ReportMediaIssueAsync(book.FilePath, MediaErrorProducer.BookService, - "There was an error reading css file for inlining likely due to a key mismatch in metadata", ex); + logger.LogError(ex, "There was an error reading css file for inlining likely due to a key mismatch in metadata"); + await mediaErrorService.ReportMediaIssueAsync(book.FilePath ?? string.Empty, MediaErrorProducer.BookService, + "There was an error reading css file for inlining likely due to a key mismatch in metadata", ex, ct); } } } @@ -550,7 +518,8 @@ public partial class BookService : IBookService .Select(l => l.Language) .FirstOrDefault()) }; - ComicInfo.CleanComicInfo(info); + + info.CleanComicInfo(); var weblinks = new List(); if (epubBook?.Schema.Package.Metadata.Identifiers != null) @@ -564,7 +533,7 @@ public partial class BookService : IBookService var isbn = identifier.Identifier.Replace("urn:isbn:", string.Empty).Replace("isbn:", string.Empty); if (!ArticleNumberHelper.IsValidIsbn10(isbn) && !ArticleNumberHelper.IsValidIsbn13(isbn)) { - _logger.LogDebug("[BookService] {File} has invalid ISBN number", filePath); + logger.LogDebug("[BookService] {File} has invalid ISBN number", filePath); continue; } @@ -683,8 +652,8 @@ public partial class BookService : IBookService } catch (Exception ex) { - _logger.LogWarning(ex, "[GetComicInfo] There was an exception parsing metadata: {FilePath}", filePath); - _mediaErrorService.ReportMediaIssue(filePath, MediaErrorProducer.BookService, + logger.LogWarning(ex, "[GetComicInfo] There was an exception parsing metadata: {FilePath}", filePath); + mediaErrorService.ReportMediaIssue(filePath, MediaErrorProducer.BookService, "There was an exception parsing metadata", ex); } finally @@ -697,17 +666,17 @@ public partial class BookService : IBookService private EpubBookRef? OpenEpubWithFallback(string filePath, EpubBookRef? epubBook) { - // TODO: Refactor this to use the Async version + // default: Refactor this to use the Async version try { epubBook = EpubReader.OpenBook(filePath, BookReaderOptions); } catch (Exception ex) { - _logger.LogWarning(ex, + logger.LogWarning(ex, "[GetComicInfo] There was an exception parsing metadata, falling back to a more lenient parsing method: {FilePath}", filePath); - _mediaErrorService.ReportMediaIssue(filePath, MediaErrorProducer.BookService, + mediaErrorService.ReportMediaIssue(filePath, MediaErrorProducer.BookService, "There was an exception parsing metadata", ex); } finally @@ -850,13 +819,13 @@ public partial class BookService : IBookService { if (!File.Exists(filePath)) { - _logger.LogWarning("[BookService] Book {EpubFile} could not be found", filePath); + logger.LogWarning("[BookService] Book {EpubFile} could not be found", filePath); return false; } if (Parser.IsBook(filePath)) return true; - _logger.LogWarning("[BookService] Book {EpubFile} is not a valid EPUB/PDF", filePath); + logger.LogWarning("[BookService] Book {EpubFile} is not a valid EPUB/PDF", filePath); return false; } @@ -877,8 +846,8 @@ public partial class BookService : IBookService } catch (Exception ex) { - _logger.LogWarning(ex, "[BookService] There was an exception getting number of pages, defaulting to 0"); - _mediaErrorService.ReportMediaIssue(filePath, MediaErrorProducer.BookService, + logger.LogWarning(ex, "[BookService] There was an exception getting number of pages, defaulting to 0"); + mediaErrorService.ReportMediaIssue(filePath, MediaErrorProducer.BookService, "There was an exception getting number of pages, defaulting to 0", ex); } @@ -902,7 +871,8 @@ public partial class BookService : IBookService return key.Replace("../", string.Empty); } - public async Task> CreateKeyToPageMappingAsync(EpubBookRef book) + public async Task> CreateKeyToPageMappingAsync(EpubBookRef book, + CancellationToken ct = default) { var dict = new Dictionary(); var pageCount = 0; @@ -918,13 +888,15 @@ public partial class BookService : IBookService return dict; } - public async Task?> GetWordCountsPerPage(string bookFilePath) + public async Task?> GetWordCountsPerPage(string bookFilePath, CancellationToken ct = default) { var ret = new Dictionary(); try { using var book = await EpubReader.OpenBookAsync(bookFilePath, LenientBookReaderOptions); - var mappings = await CreateKeyToPageMappingAsync(book); + if (book == null) return null; + + var mappings = await CreateKeyToPageMappingAsync(book, ct); var doc = new HtmlDocument {OptionFixNestedTags = true}; @@ -945,7 +917,7 @@ public partial class BookService : IBookService } catch (Exception ex) { - _logger.LogError(ex, "There was an issue calculating word counts per page"); + logger.LogError(ex, "There was an issue calculating word counts per page"); return null; } @@ -959,7 +931,7 @@ public partial class BookService : IBookService // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if (body == null) { - _logger.LogError("{FilePath} has no body tag! Generating one for support. Book may be skewed", book.FilePath); + logger.LogError("{FilePath} has no body tag! Generating one for support. Book may be skewed", book.FilePath); doc.DocumentNode.SelectSingleNode("/html").AppendChild(HtmlNode.CreateNode("")); body = doc.DocumentNode.SelectSingleNode("//html/body"); } @@ -1002,6 +974,7 @@ public partial class BookService : IBookService #region Count Letters Between XPaths + /// /// Counts the (estimated) words for a given book from a starting xpath (or beginning if null) to and ending xpath. /// May cross page boundaries @@ -1011,8 +984,10 @@ public partial class BookService : IBookService /// Page number of starting xpath /// /// Page number of ending xpath + /// /// - public async Task GetWordCountBetweenXPaths(string bookFilePath, string startXpath, int startPage, string endXpath, int endPage) + public async Task GetWordCountBetweenXPaths(string bookFilePath, string startXpath, int startPage, + string endXpath, int endPage, CancellationToken ct = default) { if (string.IsNullOrEmpty(endXpath)) return 0; if (endPage < startPage) return 0; @@ -1091,7 +1066,7 @@ public partial class BookService : IBookService } catch (Exception ex) { - _logger.LogError(ex, "There was an issue calculating word counts between XPaths"); + logger.LogError(ex, "There was an issue calculating word counts between XPaths"); return 0; } @@ -1177,7 +1152,8 @@ public partial class BookService : IBookService return textNodes?.Sum(node => node.InnerText.Count(char.IsLetter)) ?? 0; } - public async Task CopyImageToTempFromBook(int chapterId, BookmarkDto bookmarkDto, string cachedBookPath) + public async Task CopyImageToTempFromBook(int chapterId, BookmarkDto bookmarkDto, string cachedBookPath, + CancellationToken ct = default) { using var book = await EpubReader.OpenBookAsync(cachedBookPath, LenientBookReaderOptions); @@ -1256,15 +1232,15 @@ public partial class BookService : IBookService } // Create temp directory for this chapter if it doesn't exist - var tempChapterDir = Path.Combine(_directoryService.TempDirectory, chapterId.ToString()); - _directoryService.ExistOrCreate(tempChapterDir); + var tempChapterDir = Path.Combine(directoryService.TempDirectory, chapterId.ToString()); + directoryService.ExistOrCreate(tempChapterDir); // Generate unique filename var uniqueFilename = $"{Guid.NewGuid()}{extension}"; var tempFilePath = Path.Combine(tempChapterDir, uniqueFilename); // Write the image to the temp file - await File.WriteAllBytesAsync(tempFilePath, imageContent); + await File.WriteAllBytesAsync(tempFilePath, imageContent, ct); return tempFilePath; } @@ -1293,8 +1269,10 @@ public partial class BookService : IBookService /// /// /// + /// /// - public async Task GetResourceAsync(string bookFilePath, string requestedKey) + public async Task GetResourceAsync(string bookFilePath, string requestedKey, + CancellationToken ct = default) { using var book = await EpubReader.OpenBookAsync(bookFilePath, LenientBookReaderOptions); var key = CoalesceKeyForAnyFile(book, requestedKey); @@ -1318,7 +1296,7 @@ public partial class BookService : IBookService /// public ParserInfo? ParseInfo(string filePath) { - if (!Parser.IsEpub(filePath) || !_directoryService.FileSystem.File.Exists(filePath)) return null; + if (!Parser.IsEpub(filePath) || !directoryService.FileSystem.File.Exists(filePath)) return null; try { @@ -1415,8 +1393,8 @@ public partial class BookService : IBookService } catch (Exception ex) { - _logger.LogWarning(ex, "[BookService] There was an exception when opening epub book: {FileName}", filePath); - _mediaErrorService.ReportMediaIssue(filePath, MediaErrorProducer.BookService, + logger.LogWarning(ex, "[BookService] There was an exception when opening epub book: {FileName}", filePath); + mediaErrorService.ReportMediaIssue(filePath, MediaErrorProducer.BookService, "There was an exception when opening epub book", ex); } @@ -1430,9 +1408,9 @@ public partial class BookService : IBookService /// public void ExtractPdfImages(string fileFilePath, string targetDirectory) { - _directoryService.ExistOrCreate(targetDirectory); + directoryService.ExistOrCreate(targetDirectory); - var settings = _unitOfWork.SettingsRepository.GetSettingsDtoAsync().GetAwaiter().GetResult(); + var settings = unitOfWork.SettingsRepository.GetSettingsDtoAsync().GetAwaiter().GetResult(); var dims = settings.PdfRenderResolution.GetDimensions(); var pageDimensions = new PageDimensions(dims.dim1, dims.dim2); @@ -1477,11 +1455,14 @@ public partial class BookService : IBookService /// Epub mappings /// Page number we are loading /// Ptoc (Text) Bookmarks to tie against + /// + /// /// private async Task ScopePage(HtmlDocument doc, EpubBookRef book, string apiBase, HtmlNode body, - Dictionary mappings, int page, List ptocBookmarks, List annotations) + Dictionary mappings, int page, List ptocBookmarks, List annotations, + CancellationToken ct = default) { - await InlineStyles(doc, book, apiBase, body); + await InlineStyles(doc, book, apiBase, body, ct); RewriteAnchors(page, doc, mappings); @@ -1555,11 +1536,15 @@ public partial class BookService : IBookService /// this is used to rewrite anchors in the book text so that we always load properly in our reader. /// /// Chapter with at least one file + /// /// - public async Task> GenerateTableOfContents(Chapter chapter) + public async Task> GenerateTableOfContents(Chapter chapter, + CancellationToken ct = default) { using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, LenientBookReaderOptions); - var mappings = await CreateKeyToPageMappingAsync(book); + if (book == null) return []; + + var mappings = await CreateKeyToPageMappingAsync(book, ct); var navItems = await book.GetNavigationAsync(); var chaptersList = new List(); @@ -1585,7 +1570,7 @@ public partial class BookService : IBookService k.Equals("NAVIGATION.XHTML", StringComparison.InvariantCultureIgnoreCase)); if (string.IsNullOrEmpty(tocPage)) return chaptersList; - if (!book.Content.Html.TryGetLocalFileRefByKey(tocPage, out var file)) return chaptersList; + if (!book.Content.Html.TryGetLocalFileRefByKey(tocPage, out var file) || file == null) return chaptersList; var content = await file.ReadContentAsync(); var doc = new HtmlDocument(); @@ -1688,22 +1673,16 @@ public partial class BookService : IBookService return path.Substring(startIndex); } - /// - /// This returns a single page within the epub book. All html will be rewritten to be scoped within our reader, - /// all css is scoped, etc. - /// - /// The requested page - /// The chapterId - /// The path to the cached epub file - /// The API base for Kavita, to rewrite urls to so we load though our endpoint - /// Full epub HTML Page, scoped to Kavita's reader - /// All exceptions throw this - public async Task GetBookPage(int page, int chapterId, string cachedEpubPath, string baseUrl, - List ptocBookmarks, List annotations) + public async Task GetBookPage(int userId, int page, int chapterId, string cachedEpubPath, string baseUrl, + List ptocBookmarks, List annotations, CancellationToken ct = default) { + var authKey = (await unitOfWork.UserRepository.GetAuthKeysForUserId(userId, ct)) + .First(k => k is { Name: AuthKeyHelper.ImageOnlyKeyName, Provider: AuthKeyProvider.System }) + .Key; + using var book = await EpubReader.OpenBookAsync(cachedEpubPath, LenientBookReaderOptions); - var mappings = await CreateKeyToPageMappingAsync(book); - var apiBase = baseUrl + "book/" + chapterId + "/" + BookApiUrl; + var mappings = await CreateKeyToPageMappingAsync(book, ct); + var apiBase = baseUrl + "book/" + chapterId + "/" + string.Format(BookApiUrl, authKey); var counter = 0; var doc = new HtmlDocument {OptionFixNestedTags = true}; @@ -1739,18 +1718,18 @@ public partial class BookService : IBookService LogBookErrors(book, contentFileRef, doc); throw new KavitaException("epub-malformed"); } - _logger.LogError("{FilePath} has no body tag! Generating one for support. Book may be skewed", book.FilePath); + logger.LogError("{FilePath} has no body tag! Generating one for support. Book may be skewed", book.FilePath); doc.DocumentNode.SelectSingleNode("/html").AppendChild(HtmlNode.CreateNode("")); body = doc.DocumentNode.SelectSingleNode("/html/body"); } - return await ScopePage(doc, book, apiBase, body!, mappings, page, ptocBookmarks, annotations); + return await ScopePage(doc, book, apiBase, body!, mappings, page, ptocBookmarks, annotations, ct); } } catch (Exception ex) { - _logger.LogError(ex, "There was an issue reading one of the pages for {Book}", book.FilePath); - await _mediaErrorService.ReportMediaIssueAsync(book.FilePath, MediaErrorProducer.BookService, - "There was an issue reading one of the pages for", ex); + logger.LogError(ex, "There was an issue reading one of the pages for {Book}", book.FilePath); + await mediaErrorService.ReportMediaIssueAsync(book.FilePath ?? string.Empty, MediaErrorProducer.BookService, + "There was an issue reading one of the pages for", ex, ct); } throw new KavitaException("epub-html-missing"); @@ -1774,6 +1753,7 @@ public partial class BookService : IBookService } using var epubBook = EpubReader.OpenBook(fileFilePath, LenientBookReaderOptions); + if (epubBook == null) return string.Empty; try { @@ -1785,12 +1765,12 @@ public partial class BookService : IBookService if (coverImageContent == null) return string.Empty; using var stream = coverImageContent.GetContentStream(); - return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat, size); + return imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat, size); } catch (Exception ex) { - _logger.LogWarning(ex, "[BookService] There was a critical error and prevented thumbnail generation on {BookFile}. Defaulting to no cover image", fileFilePath); - _mediaErrorService.ReportMediaIssue(fileFilePath, MediaErrorProducer.BookService, + logger.LogWarning(ex, "[BookService] There was a critical error and prevented thumbnail generation on {BookFile}. Defaulting to no cover image", fileFilePath); + mediaErrorService.ReportMediaIssue(fileFilePath, MediaErrorProducer.BookService, "There was a critical error and prevented thumbnail generation", ex); } @@ -1830,15 +1810,15 @@ public partial class BookService : IBookService using var stream = StreamManager.GetStream("BookService.GetPdfPage"); GetPdfPage(docReader, 0, stream); - return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat, size); + return imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat, size); } catch (Exception ex) { - _logger.LogWarning(ex, + logger.LogWarning(ex, "[BookService] There was a critical error and prevented thumbnail generation on {BookFile}. Defaulting to no cover image", fileFilePath); - _mediaErrorService.ReportMediaIssue(fileFilePath, MediaErrorProducer.BookService, + mediaErrorService.ReportMediaIssue(fileFilePath, MediaErrorProducer.BookService, "There was a critical error and prevented thumbnail generation", ex); } @@ -1902,10 +1882,10 @@ public partial class BookService : IBookService private void LogBookErrors(EpubBookRef book, EpubContentFileRef contentFileRef, HtmlDocument doc) { - _logger.LogError("{FilePath} has an invalid html file (Page {PageName})", book.FilePath, contentFileRef.Key); + logger.LogError("{FilePath} has an invalid html file (Page {PageName})", book.FilePath, contentFileRef.Key); foreach (var error in doc.ParseErrors) { - _logger.LogError("Line {LineNumber}, Reason: {Reason}", error.Line, error.Reason); + logger.LogError("Line {LineNumber}, Reason: {Reason}", error.Line, error.Reason); } } diff --git a/API/Services/BookmarkService.cs b/Kavita.Services/BookmarkService.cs similarity index 53% rename from API/Services/BookmarkService.cs rename to Kavita.Services/BookmarkService.cs index 9cd72dcba..c4bfdc5fd 100644 --- a/API/Services/BookmarkService.cs +++ b/Kavita.Services/BookmarkService.cs @@ -2,71 +2,58 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.DTOs.Reader; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Models.Extensions; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; -namespace API.Services; +namespace Kavita.Services; -#nullable enable -public interface IBookmarkService -{ - Task DeleteBookmarkFiles(IEnumerable bookmarks); - Task BookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto, string imageToBookmark); - Task RemoveBookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto); - Task> GetBookmarkFilesById(IEnumerable bookmarkIds); -} - -public class BookmarkService : IBookmarkService +public class BookmarkService( + ILogger logger, + IUnitOfWork unitOfWork, + IDirectoryService directoryService, + IMediaConversionService mediaConversionService) + : IBookmarkService { public const string Name = "BookmarkService"; - private readonly ILogger _logger; - private readonly IUnitOfWork _unitOfWork; - private readonly IDirectoryService _directoryService; - private readonly IMediaConversionService _mediaConversionService; - - public BookmarkService(ILogger logger, IUnitOfWork unitOfWork, - IDirectoryService directoryService, IMediaConversionService mediaConversionService) - { - _logger = logger; - _unitOfWork = unitOfWork; - _directoryService = directoryService; - _mediaConversionService = mediaConversionService; - } /// /// Deletes the files associated with the list of Bookmarks passed. Will clean up empty folders. /// /// - public async Task DeleteBookmarkFiles(IEnumerable bookmarks) + /// + public async Task DeleteBookmarkFiles(IEnumerable bookmarks, CancellationToken ct = default) { var bookmarkDirectory = - (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; + (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory, ct)).Value; var bookmarkFilesToDelete = bookmarks .Where(b => b != null) - .Select(b => Tasks.Scanner.Parser.Parser.NormalizePath( - _directoryService.FileSystem.Path.Join(bookmarkDirectory, b!.FileName))) + .Select(b => Parser.NormalizePath( + directoryService.FileSystem.Path.Join(bookmarkDirectory, b!.FileName))) .ToList(); if (bookmarkFilesToDelete.Count == 0) return; - _directoryService.DeleteFiles(bookmarkFilesToDelete); + directoryService.DeleteFiles(bookmarkFilesToDelete); // Delete any leftover folders - foreach (var directory in _directoryService.FileSystem.Directory.GetDirectories(bookmarkDirectory, string.Empty, SearchOption.AllDirectories)) + foreach (var directory in directoryService.FileSystem.Directory.GetDirectories(bookmarkDirectory, string.Empty, SearchOption.AllDirectories)) { - if (_directoryService.FileSystem.Directory.GetFiles(directory, "", SearchOption.AllDirectories).Length == 0 && - _directoryService.FileSystem.Directory.GetDirectories(directory).Length == 0) + if (directoryService.FileSystem.Directory.GetFiles(directory, "", SearchOption.AllDirectories).Length == 0 && + directoryService.FileSystem.Directory.GetDirectories(directory).Length == 0) { - _directoryService.FileSystem.Directory.Delete(directory, false); + directoryService.FileSystem.Directory.Delete(directory, false); } } } @@ -75,21 +62,21 @@ public class BookmarkService : IBookmarkService /// This is a job that runs after a bookmark is saved /// /// This must be public - public async Task ConvertBookmarkToEncoding(int bookmarkId) + public async Task ConvertBookmarkToEncoding(int bookmarkId, CancellationToken ct = default) { var bookmarkDirectory = - (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; + (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory, ct)).Value; var encodeFormat = - (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; + (await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct)).EncodeMediaAs; if (encodeFormat == EncodeFormat.PNG) { - _logger.LogError("Cannot convert media to PNG"); + logger.LogError("Cannot convert media to PNG"); return; } // Validate the bookmark still exists - var bookmark = await _unitOfWork.UserRepository.GetBookmarkAsync(bookmarkId); + var bookmark = await unitOfWork.UserRepository.GetBookmarkAsync(bookmarkId, ct); if (bookmark == null) return; // Validate the bookmark isn't already in target format @@ -99,11 +86,11 @@ public class BookmarkService : IBookmarkService return; } - bookmark.FileName = await _mediaConversionService.SaveAsEncodingFormat(bookmarkDirectory, bookmark.FileName, + bookmark.FileName = await mediaConversionService.SaveAsEncodingFormat(bookmarkDirectory, bookmark.FileName, BookmarkStem(bookmark.AppUserId, bookmark.SeriesId, bookmark.ChapterId), encodeFormat); - _unitOfWork.UserRepository.Update(bookmark); + unitOfWork.UserRepository.Update(bookmark); - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); } @@ -113,8 +100,10 @@ public class BookmarkService : IBookmarkService /// An AppUser object with Bookmarks populated /// /// Full path to the cached image that is going to be copied + /// /// If the save to DB and copy was successful - public async Task BookmarkPage(AppUser? userWithBookmarks, BookmarkDto bookmarkDto, string imageToBookmark) + public async Task BookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto, string imageToBookmark, + CancellationToken ct = default) { if (userWithBookmarks?.Bookmarks == null) { @@ -127,12 +116,12 @@ public class BookmarkService : IBookmarkService .SingleOrDefault(b => b.Page == bookmarkDto.Page && b.ChapterId == bookmarkDto.ChapterId && b.ImageOffset == bookmarkDto.ImageOffset); if (userBookmark != null) { - _logger.LogError("Bookmark already exists for Series {SeriesId}, Volume {VolumeId}, Chapter {ChapterId}, Page {PageNum}", bookmarkDto.SeriesId, bookmarkDto.VolumeId, bookmarkDto.ChapterId, bookmarkDto.Page); + logger.LogError("Bookmark already exists for Series {SeriesId}, Volume {VolumeId}, Chapter {ChapterId}, Page {PageNum}", bookmarkDto.SeriesId, bookmarkDto.VolumeId, bookmarkDto.ChapterId, bookmarkDto.Page); return true; } - var fileInfo = _directoryService.FileSystem.FileInfo.New(imageToBookmark); - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var fileInfo = directoryService.FileSystem.FileInfo.New(imageToBookmark); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); var targetFolderStem = BookmarkStem(userWithBookmarks.Id, bookmarkDto.SeriesId, bookmarkDto.ChapterId); var targetFilepath = Path.Join(settings.BookmarksDirectory, targetFolderStem); @@ -149,10 +138,10 @@ public class BookmarkService : IBookmarkService AppUserId = userWithBookmarks.Id }; - _directoryService.CopyFileToDirectory(imageToBookmark, targetFilepath); + directoryService.CopyFileToDirectory(imageToBookmark, targetFilepath); - _unitOfWork.UserRepository.Add(bookmark); - await _unitOfWork.CommitAsync(); + unitOfWork.UserRepository.Add(bookmark); + await unitOfWork.CommitAsync(ct); if (settings.EncodeMediaAs != EncodeFormat.PNG) { @@ -162,8 +151,8 @@ public class BookmarkService : IBookmarkService } catch (Exception ex) { - _logger.LogError(ex, "There was an exception when saving bookmark"); - await _unitOfWork.RollbackAsync(); + logger.LogError(ex, "There was an exception when saving bookmark"); + await unitOfWork.RollbackAsync(ct); return false; } @@ -175,8 +164,10 @@ public class BookmarkService : IBookmarkService /// /// /// + /// /// - public async Task RemoveBookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto) + public async Task RemoveBookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto, + CancellationToken ct = default) { var bookmarkToDelete = userWithBookmarks.Bookmarks.FirstOrDefault(x => x.ChapterId == bookmarkDto.ChapterId && x.Page == bookmarkDto.Page && x.ImageOffset == bookmarkDto.ImageOffset); @@ -184,33 +175,33 @@ public class BookmarkService : IBookmarkService { if (bookmarkToDelete != null) { - _unitOfWork.UserRepository.Delete(bookmarkToDelete); + unitOfWork.UserRepository.Delete(bookmarkToDelete); } - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); } catch (Exception) { return false; } - await DeleteBookmarkFiles(new[] {bookmarkToDelete}); + await DeleteBookmarkFiles([bookmarkToDelete], ct); return true; } - public async Task> GetBookmarkFilesById(IEnumerable bookmarkIds) + public async Task> GetBookmarkFilesById(IEnumerable bookmarkIds, + CancellationToken ct = default) { var bookmarkDirectory = - (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; + (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory, ct)).Value; + + var bookmarks = await unitOfWork.UserRepository.GetAllBookmarksByIds(bookmarkIds.ToList(), ct); - var bookmarks = await _unitOfWork.UserRepository.GetAllBookmarksByIds(bookmarkIds.ToList()); return bookmarks - .Select(b => Tasks.Scanner.Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(bookmarkDirectory, + .Select(b => Parser.NormalizePath(directoryService.FileSystem.Path.Join(bookmarkDirectory, b.FileName))); } - - public static string BookmarkStem(int userId, int seriesId, int chapterId) { return Path.Join($"{userId}", $"{seriesId}", $"{chapterId}"); diff --git a/API/Helpers/Builders/ChapterBuilder.cs b/Kavita.Services/Builders/ChapterBuilder.cs similarity index 94% rename from API/Helpers/Builders/ChapterBuilder.cs rename to Kavita.Services/Builders/ChapterBuilder.cs index 75c86337d..5eae3d9ca 100644 --- a/API/Helpers/Builders/ChapterBuilder.cs +++ b/Kavita.Services/Builders/ChapterBuilder.cs @@ -1,13 +1,15 @@ using System; using System.Collections.Generic; using System.Globalization; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Person; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Models.Builders; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Person; +using Kavita.Models.Parser; +using Kavita.Services.Extensions; +using Kavita.Services.Scanner; -namespace API.Helpers.Builders; -#nullable enable +namespace Kavita.Services.Builders; public class ChapterBuilder : IEntityBuilder { diff --git a/API/Helpers/Builders/MangaFileBuilder.cs b/Kavita.Services/Builders/MangaFileBuilder.cs similarity index 90% rename from API/Helpers/Builders/MangaFileBuilder.cs rename to Kavita.Services/Builders/MangaFileBuilder.cs index 480785d8f..efe938f9c 100644 --- a/API/Helpers/Builders/MangaFileBuilder.cs +++ b/Kavita.Services/Builders/MangaFileBuilder.cs @@ -1,10 +1,12 @@ using System; using System.IO; -using API.Entities; -using API.Entities.Enums; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Models.Builders; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Helpers; +using Kavita.Services.Scanner; -namespace API.Helpers.Builders; +namespace Kavita.Services.Builders; public class MangaFileBuilder : IEntityBuilder { @@ -67,8 +69,6 @@ public class MangaFileBuilder : IEntityBuilder /// Only applicable to Epubs public MangaFileBuilder WithHash() { - //if (_mangaFile.Format != MangaFormat.Epub) return this; - _mangaFile.KoreaderHash = KoreaderHelper.HashContents(_mangaFile.FilePath); return this; diff --git a/API/Helpers/Builders/VolumeBuilder.cs b/Kavita.Services/Builders/VolumeBuilder.cs similarity index 88% rename from API/Helpers/Builders/VolumeBuilder.cs rename to Kavita.Services/Builders/VolumeBuilder.cs index 13f8aae94..559a628c8 100644 --- a/API/Helpers/Builders/VolumeBuilder.cs +++ b/Kavita.Services/Builders/VolumeBuilder.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; -using API.Entities; +using API.Helpers.Builders; +using Kavita.Models.Entities; +using Kavita.Services.Scanner; -namespace API.Helpers.Builders; +namespace Kavita.Services.Builders; public class VolumeBuilder : IEntityBuilder { @@ -16,8 +18,8 @@ public class VolumeBuilder : IEntityBuilder { Name = volumeNumber, LookupName = volumeNumber, - MinNumber = Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(volumeNumber), - MaxNumber = Services.Tasks.Scanner.Parser.Parser.MaxNumberFromRange(volumeNumber), + MinNumber = Parser.MinNumberFromRange(volumeNumber), + MaxNumber = Parser.MaxNumberFromRange(volumeNumber), Chapters = new List() }; } diff --git a/API/Services/CacheService.cs b/Kavita.Services/CacheService.cs similarity index 60% rename from API/Services/CacheService.cs rename to Kavita.Services/CacheService.cs index 9df4d5671..511b5d390 100644 --- a/API/Services/CacheService.cs +++ b/Kavita.Services/CacheService.cs @@ -5,71 +5,33 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.DTOs.Reader; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NetVips; -namespace API.Services; -#nullable enable +namespace Kavita.Services; -public interface ICacheService +public class CacheService( + ILogger logger, + IUnitOfWork unitOfWork, + IDirectoryService directoryService, + IReadingItemService readingItemService, + IBookmarkService bookmarkService) + : ICacheService { - /// - /// Ensures the cache is created for the given chapter and if not, will create it. Should be called before any other - /// cache operations (except cleanup). - /// - /// - /// Extracts a PDF into images for a different reading experience - /// Chapter for the passed chapterId. Side-effect from ensuring cache. - Task Ensure(int chapterId, bool extractPdfToImages = false); - /// - /// Clears cache directory of all volumes. This can be invoked from deleting a library or a series. - /// - /// Volumes that belong to that library. Assume the library might have been deleted before this invocation. - void CleanupChapters(IEnumerable chapterIds); - void CleanupBookmarks(IEnumerable seriesIds); - string GetCachedPagePath(int chapterId, int page); - string GetCachePath(int chapterId); - string GetBookmarkCachePath(int seriesId); - IEnumerable GetCachedPages(int chapterId); - IEnumerable GetCachedFileDimensions(string cachePath); - string GetCachedBookmarkPagePath(int seriesId, int page); - string GetCachedFile(Chapter chapter); - string GetCachedFile(int chapterId, string firstFilePath); - public void ExtractChapterFiles(string extractPath, IReadOnlyList files, bool extractPdfImages = false); - Task CacheBookmarkForSeries(int userId, int seriesId); - void CleanupBookmarkCache(int seriesId); -} -public class CacheService : ICacheService -{ - private readonly ILogger _logger; - private readonly IUnitOfWork _unitOfWork; - private readonly IDirectoryService _directoryService; - private readonly IReadingItemService _readingItemService; - private readonly IBookmarkService _bookmarkService; - private static readonly ConcurrentDictionary ExtractLocks = new(); - public CacheService(ILogger logger, IUnitOfWork unitOfWork, - IDirectoryService directoryService, IReadingItemService readingItemService, - IBookmarkService bookmarkService) - { - _logger = logger; - _unitOfWork = unitOfWork; - _directoryService = directoryService; - _readingItemService = readingItemService; - _bookmarkService = bookmarkService; - } - public IEnumerable GetCachedPages(int chapterId) { var path = GetCachePath(chapterId); - return _directoryService.GetFilesWithExtension(path, Tasks.Scanner.Parser.Parser.ImageFileExtensions) + return directoryService.GetFilesWithExtension(path, Parser.ImageFileExtensions) .OrderByNatural(Path.GetFileNameWithoutExtension); } @@ -80,7 +42,7 @@ public class CacheService : ICacheService /// public IEnumerable GetCachedFileDimensions(string cachePath) { - var files = _directoryService.GetFilesWithExtension(cachePath, Tasks.Scanner.Parser.Parser.ImageFileExtensions) + var files = directoryService.GetFilesWithExtension(cachePath, Parser.ImageFileExtensions) .OrderByNatural(Path.GetFileNameWithoutExtension) .ToArray(); @@ -110,7 +72,7 @@ public class CacheService : ICacheService } catch (Exception ex) { - _logger.LogError(ex, "There was an error calculating image dimensions for {CachePath}", cachePath); + logger.LogError(ex, "There was an error calculating image dimensions for {CachePath}", cachePath); } finally { @@ -124,7 +86,7 @@ public class CacheService : ICacheService { // Calculate what chapter the page belongs to var path = GetBookmarkCachePath(seriesId); - var files = _directoryService.GetFilesWithExtension(path, Tasks.Scanner.Parser.Parser.ImageFileExtensions); + var files = directoryService.GetFilesWithExtension(path, Parser.ImageFileExtensions); files = files .AsEnumerable() .OrderByNatural(Path.GetFileNameWithoutExtension) @@ -147,8 +109,8 @@ public class CacheService : ICacheService public string GetCachedFile(Chapter chapter) { var extractPath = GetCachePath(chapter.Id); - var path = Path.Join(extractPath, _directoryService.FileSystem.Path.GetFileName(chapter.Files.First().FilePath)); - if (!(_directoryService.FileSystem.FileInfo.New(path).Exists)) + var path = Path.Join(extractPath, directoryService.FileSystem.Path.GetFileName(chapter.Files.First().FilePath)); + if (!(directoryService.FileSystem.FileInfo.New(path).Exists)) { path = chapter.Files.First().FilePath; } @@ -158,8 +120,8 @@ public class CacheService : ICacheService public string GetCachedFile(int chapterId, string firstFilePath) { var extractPath = GetCachePath(chapterId); - var path = Path.Join(extractPath, _directoryService.FileSystem.Path.GetFileName(firstFilePath)); - if (!(_directoryService.FileSystem.FileInfo.New(path).Exists)) + var path = Path.Join(extractPath, directoryService.FileSystem.Path.GetFileName(firstFilePath)); + if (!(directoryService.FileSystem.FileInfo.New(path).Exists)) { path = firstFilePath; } @@ -172,23 +134,24 @@ public class CacheService : ICacheService /// /// /// Defaults to false. Extract pdf file into images rather than copying just the pdf file + /// /// This will always return the Chapter for the chapterId - public async Task Ensure(int chapterId, bool extractPdfToImages = false) + public async Task Ensure(int chapterId, bool extractPdfToImages = false, CancellationToken ct = default) { - _directoryService.ExistOrCreate(_directoryService.CacheDirectory); - var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId); + directoryService.ExistOrCreate(directoryService.CacheDirectory); + var chapter = await unitOfWork.ChapterRepository.GetChapterAsync(chapterId, ct: ct); var extractPath = GetCachePath(chapterId); var extractLock = ExtractLocks.GetOrAdd(chapterId, id => new SemaphoreSlim(1,1)); - await extractLock.WaitAsync(); + await extractLock.WaitAsync(ct); + try { - if (_directoryService.Exists(extractPath)) + if (directoryService.Exists(extractPath)) { if (extractPdfToImages) { - var pdfImages = _directoryService.GetFiles(extractPath, - Tasks.Scanner.Parser.Parser.ImageFileExtensions); + var pdfImages = directoryService.GetFiles(extractPath, Parser.ImageFileExtensions); if (pdfImages.Any()) { return chapter; @@ -198,13 +161,13 @@ public class CacheService : ICacheService { // Do an explicit check for files since rarely a "permission denied" error on deleting // the file can occur, thus leaving an empty folder and we would never re-cache the files. - if (_directoryService.GetFiles(extractPath).Any()) + if (directoryService.GetFiles(extractPath).Any()) { return chapter; } // Delete the extractPath as ExtractArchive will return if the directory already exists - _directoryService.ClearAndDeleteDirectory(extractPath); + directoryService.ClearAndDeleteDirectory(extractPath); } } @@ -231,15 +194,15 @@ public class CacheService : ICacheService var removeNonImages = true; var fileCount = files.Count; var extraPath = string.Empty; - var extractDi = _directoryService.FileSystem.DirectoryInfo.New(extractPath); + var extractDi = directoryService.FileSystem.DirectoryInfo.New(extractPath); if (files[0].Format == MangaFormat.Image) { // Check if all the files are Images. If so, do a directory copy, else do the normal copy if (files.All(f => f.Format == MangaFormat.Image)) { - _directoryService.ExistOrCreate(extractPath); - _directoryService.CopyFilesToDirectory(files.Select(f => f.FilePath), extractPath); + directoryService.ExistOrCreate(extractPath); + directoryService.CopyFilesToDirectory(files.Select(f => f.FilePath), extractPath); } else { @@ -249,9 +212,9 @@ public class CacheService : ICacheService { extraPath = file.Id + string.Empty; } - _readingItemService.Extract(file.FilePath, Path.Join(extractPath, extraPath), MangaFormat.Image, files.Count); + readingItemService.Extract(file.FilePath, Path.Join(extractPath, extraPath), MangaFormat.Image, files.Count); } - _directoryService.Flatten(extractDi.FullName); + directoryService.Flatten(extractDi.FullName); } } @@ -266,34 +229,34 @@ public class CacheService : ICacheService switch (file.Format) { case MangaFormat.Archive: - _readingItemService.Extract(file.FilePath, Path.Join(extractPath, extraPath), file.Format); + readingItemService.Extract(file.FilePath, Path.Join(extractPath, extraPath), file.Format); break; case MangaFormat.Epub: case MangaFormat.Pdf: { - if (!_directoryService.FileSystem.File.Exists(files[0].FilePath)) + if (!directoryService.FileSystem.File.Exists(files[0].FilePath)) { - _logger.LogError("{File} does not exist on disk", files[0].FilePath); + logger.LogError("{File} does not exist on disk", files[0].FilePath); throw new KavitaException($"{files[0].FilePath} does not exist on disk"); } if (extractPdfImages) { - _readingItemService.Extract(file.FilePath, Path.Join(extractPath, extraPath), file.Format); + readingItemService.Extract(file.FilePath, Path.Join(extractPath, extraPath), file.Format); break; } removeNonImages = false; - _directoryService.ExistOrCreate(extractPath); - _directoryService.CopyFileToDirectory(files[0].FilePath, extractPath); + directoryService.ExistOrCreate(extractPath); + directoryService.CopyFileToDirectory(files[0].FilePath, extractPath); break; } } } - _directoryService.Flatten(extractDi.FullName); + directoryService.Flatten(extractDi.FullName); if (removeNonImages) { - _directoryService.RemoveNonImages(extractDi.FullName); + directoryService.RemoveNonImages(extractDi.FullName); } } @@ -305,7 +268,7 @@ public class CacheService : ICacheService { foreach (var chapter in chapterIds) { - _directoryService.ClearAndDeleteDirectory(GetCachePath(chapter)); + directoryService.ClearAndDeleteDirectory(GetCachePath(chapter)); } } @@ -317,7 +280,7 @@ public class CacheService : ICacheService { foreach (var series in seriesIds) { - _directoryService.ClearAndDeleteDirectory(GetBookmarkCachePath(series)); + directoryService.ClearAndDeleteDirectory(GetBookmarkCachePath(series)); } } @@ -329,7 +292,7 @@ public class CacheService : ICacheService /// public string GetCachePath(int chapterId) { - return _directoryService.FileSystem.Path.GetFullPath(_directoryService.FileSystem.Path.Join(_directoryService.CacheDirectory, $"{chapterId}/")); + return directoryService.FileSystem.Path.GetFullPath(directoryService.FileSystem.Path.Join(directoryService.CacheDirectory, $"{chapterId}/")); } /// @@ -339,7 +302,7 @@ public class CacheService : ICacheService /// public string GetBookmarkCachePath(int seriesId) { - return _directoryService.FileSystem.Path.GetFullPath(_directoryService.FileSystem.Path.Join(_directoryService.CacheDirectory, $"{seriesId}_bookmarks/")); + return directoryService.FileSystem.Path.GetFullPath(directoryService.FileSystem.Path.Join(directoryService.CacheDirectory, $"{seriesId}_bookmarks/")); } /// @@ -353,20 +316,22 @@ public class CacheService : ICacheService // Calculate what chapter the page belongs to var path = GetCachePath(chapterId); // NOTE: We can optimize this by extracting and renaming, so we don't need to scan for the files and can do a direct access - var files = _directoryService.GetFilesWithExtension(path, Tasks.Scanner.Parser.Parser.ImageFileExtensions); + var files = directoryService.GetFilesWithExtension(path, Parser.ImageFileExtensions); return GetPageFromFiles(files, page); } - public async Task CacheBookmarkForSeries(int userId, int seriesId) + public async Task CacheBookmarkForSeries(int userId, int seriesId, CancellationToken ct = default) { - var destDirectory = _directoryService.FileSystem.Path.Join(_directoryService.CacheDirectory, seriesId + "_bookmarks"); - if (_directoryService.Exists(destDirectory)) return _directoryService.GetFiles(destDirectory).Count(); + var destDirectory = directoryService.FileSystem.Path.Join(directoryService.CacheDirectory, seriesId + "_bookmarks"); + if (directoryService.Exists(destDirectory)) return directoryService.GetFiles(destDirectory).Count(); - var bookmarkDtos = await _unitOfWork.UserRepository.GetBookmarkDtosForSeries(userId, seriesId); - var files = (await _bookmarkService.GetBookmarkFilesById(bookmarkDtos.Select(b => b.Id))).ToList(); - _directoryService.CopyFilesToDirectory(files, destDirectory, + var bookmarkDtos = await unitOfWork.UserRepository.GetBookmarkDtosForSeries(userId, seriesId, ct); + + var files = (await bookmarkService.GetBookmarkFilesById(bookmarkDtos.Select(b => b.Id), ct)).ToList(); + directoryService.CopyFilesToDirectory(files, destDirectory, Enumerable.Range(1, files.Count).Select(i => i + string.Empty).ToList()); + return files.Count; } @@ -376,10 +341,10 @@ public class CacheService : ICacheService /// public void CleanupBookmarkCache(int seriesId) { - var destDirectory = _directoryService.FileSystem.Path.Join(_directoryService.CacheDirectory, seriesId + "_bookmarks"); - if (!_directoryService.Exists(destDirectory)) return; + var destDirectory = directoryService.FileSystem.Path.Join(directoryService.CacheDirectory, seriesId + "_bookmarks"); + if (!directoryService.Exists(destDirectory)) return; - _directoryService.ClearAndDeleteDirectory(destDirectory); + directoryService.ClearAndDeleteDirectory(destDirectory); } /// diff --git a/Kavita.Services/CleanupService.cs b/Kavita.Services/CleanupService.cs new file mode 100644 index 000000000..d1d461127 --- /dev/null +++ b/Kavita.Services/CleanupService.cs @@ -0,0 +1,417 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Services.Scanner; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Kavita.Services; + +/// +/// Cleans up after operations on reoccurring basis +/// +public class CleanupService( + ILogger logger, + IUnitOfWork unitOfWork, + IEventHub eventHub, + IDirectoryService directoryService) + : ICleanupService +{ + /// + /// Cleans up Temp, cache, deleted cover images, and old database backups + /// + /// + [AutomaticRetry(Attempts = 3, LogEvents = false, OnAttemptsExceeded = AttemptsExceededAction.Fail, DelaysInSeconds = [120, 300, 300])] + public async Task Cleanup(CancellationToken ct = default) + { + if (TaskScheduler.HasAlreadyEnqueuedTask(BookmarkService.Name, "ConvertAllCoverToEncoding", [], + TaskScheduler.DefaultQueue, true) || + TaskScheduler.HasAlreadyEnqueuedTask(BookmarkService.Name, "ConvertAllBookmarkToEncoding", [], + TaskScheduler.DefaultQueue, true)) + { + logger.LogInformation("Cleanup put on hold as a media conversion in progress"); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ErrorEvent("Cleanup", "Cleanup put on hold as a media conversion in progress"), ct: ct); + return; + } + + logger.LogInformation("Starting Cleanup"); + + // TODO: Why do I have clear temp directory then immediately do it again? + var cleanupSteps = new List<(Func, string)> + { + (innerCt => Task.Run(() => directoryService.ClearDirectory(directoryService.TempDirectory), innerCt), "Cleaning temp directory"), + (CleanupCacheAndTempDirectories, "Cleaning cache and temp directories"), + (CleanupBackups, "Cleaning old database backups"), + (ConsolidateProgress, "Consolidating Progress Events"), + (CleanupMediaErrors, "Consolidating Media Errors"), + (CleanupDbEntries, "Cleaning abandoned database rows"), // Cleanup DB before removing files linked to DB entries + (DeleteSeriesCoverImages, "Cleaning deleted series cover images"), + (DeleteChapterCoverImages, "Cleaning deleted chapter cover images"), + (innerCt => Task.WhenAll(DeleteTagCoverImages(innerCt), DeleteReadingListCoverImages(innerCt), DeletePersonCoverImages(innerCt)), "Cleaning deleted cover images"), + (CleanupLogs, "Cleaning old logs"), + (EnsureChapterProgressIsCapped, "Cleaning progress events that exceed 100%") + }; + + await SendProgress(0F, "Starting cleanup", ct); + + for (var i = 0; i < cleanupSteps.Count; i++) + { + var (method, subtitle) = cleanupSteps[i]; + var progress = (float)(i + 1) / (cleanupSteps.Count + 1); + + logger.LogInformation("{Message}", subtitle); + await method(ct); + await SendProgress(progress, subtitle, ct); + } + + await SendProgress(1F, "Cleanup finished", ct); + logger.LogInformation("Cleanup finished"); + } + + /// + /// Cleans up abandon rows in the DB + /// + public async Task CleanupDbEntries(CancellationToken ct = default) + { + await unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters(ct); + await unitOfWork.PersonRepository.RemoveAllPeopleNoLongerAssociated(ct); + await unitOfWork.GenreRepository.RemoveAllGenreNoLongerAssociated(ct: ct); + await unitOfWork.CollectionTagRepository.RemoveCollectionsWithoutSeries(ct); + await unitOfWork.ReadingListRepository.RemoveReadingListsWithoutSeries(ct); + } + + private async Task SendProgress(float progress, string subtitle, CancellationToken ct = default) + { + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.CleanupProgressEvent(progress, subtitle), ct: ct); + } + + /// + /// Removes all series images that are not in the database. They must follow filename pattern. + /// + public async Task DeleteSeriesCoverImages(CancellationToken ct = default) + { + var images = await unitOfWork.SeriesRepository.GetAllCoverImagesAsync(); + var files = directoryService.GetFiles(directoryService.CoverImageDirectory, ImageService.SeriesCoverImageRegex); + directoryService.DeleteFiles(files.Where(file => !images.Contains(directoryService.FileSystem.Path.GetFileName(file)))); + } + + /// + /// Removes all chapter/volume images that are not in the database. They must follow filename pattern. + /// + public async Task DeleteChapterCoverImages(CancellationToken ct = default) + { + var images = await unitOfWork.ChapterRepository.GetAllCoverImagesAsync(ct); + var files = directoryService.GetFiles(directoryService.CoverImageDirectory, ImageService.ChapterCoverImageRegex); + directoryService.DeleteFiles(files.Where(file => !images.Contains(directoryService.FileSystem.Path.GetFileName(file)))); + } + + /// + /// Removes all collection tag images that are not in the database. They must follow filename pattern. + /// + public async Task DeleteTagCoverImages(CancellationToken ct = default) + { + var images = await unitOfWork.CollectionTagRepository.GetAllCoverImagesAsync(ct); + var files = directoryService.GetFiles(directoryService.CoverImageDirectory, ImageService.CollectionTagCoverImageRegex); + directoryService.DeleteFiles(files.Where(file => !images.Contains(directoryService.FileSystem.Path.GetFileName(file)))); + } + + /// + /// Removes all reading list images that are not in the database. They must follow filename pattern. + /// + public async Task DeleteReadingListCoverImages(CancellationToken ct = default) + { + var images = await unitOfWork.ReadingListRepository.GetAllCoverImagesAsync(ct); + var files = directoryService.GetFiles(directoryService.CoverImageDirectory, ImageService.ReadingListCoverImageRegex); + directoryService.DeleteFiles(files.Where(file => !images.Contains(directoryService.FileSystem.Path.GetFileName(file)))); + } + + /// + /// Remove all person cover images no longer associated with a person in the database + /// + public async Task DeletePersonCoverImages(CancellationToken ct = default) + { + var images = await unitOfWork.PersonRepository.GetAllCoverImagesAsync(ct); + var files = directoryService.GetFiles(directoryService.CoverImageDirectory, ImageService.PersonCoverImageRegex); + directoryService.DeleteFiles(files.Where(file => !images.Contains(directoryService.FileSystem.Path.GetFileName(file)))); + } + + /// + /// Removes all files and directories in the cache and temp directory + /// + public Task CleanupCacheAndTempDirectories(CancellationToken ct = default) + { + logger.LogInformation("Performing cleanup of Cache & Temp directories"); + directoryService.ExistOrCreate(directoryService.CacheDirectory); + directoryService.ExistOrCreate(directoryService.TempDirectory); + + try + { + directoryService.ClearDirectory(directoryService.CacheDirectory); + directoryService.ClearDirectory(directoryService.TempDirectory); + } + catch (Exception ex) + { + logger.LogError(ex, "There was an issue deleting one or more folders/files during cleanup"); + } + + logger.LogInformation("Cache and temp directory purged"); + + return Task.CompletedTask; + } + + public void CleanupCacheDirectory() + { + logger.LogInformation("Performing cleanup of Cache directories"); + directoryService.ExistOrCreate(directoryService.CacheDirectory); + + try + { + directoryService.ClearDirectory(directoryService.CacheDirectory); + } + catch (Exception ex) + { + logger.LogError(ex, "There was an issue deleting one or more folders/files during cleanup"); + } + + logger.LogInformation("Cache directory purged"); + } + + /// + /// Removes Database backups older than configured total backups. If all backups are older than total backups days, only the latest is kept. + /// + public async Task CleanupBackups(CancellationToken ct = default) + { + var dayThreshold = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct)).TotalBackups; + logger.LogInformation("Beginning cleanup of Database backups at {Time}", DateTime.Now); + var backupDirectory = + (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BackupDirectory, ct)).Value; + if (!directoryService.Exists(backupDirectory)) return; + + var deltaTime = DateTime.Today.Subtract(TimeSpan.FromDays(dayThreshold)); + var allBackups = directoryService.GetFiles(backupDirectory).ToList(); + var expiredBackups = allBackups.Select(filename => directoryService.FileSystem.FileInfo.New(filename)) + .Where(f => f.CreationTime < deltaTime) + .ToList(); + + if (expiredBackups.Count == allBackups.Count) + { + logger.LogInformation("All expired backups are older than {Threshold} days. Removing all but last backup", dayThreshold); + var toDelete = expiredBackups.OrderByDescending(f => f.CreationTime).ToList(); + directoryService.DeleteFiles(toDelete.Take(toDelete.Count - 1).Select(f => f.FullName)); + } + else + { + directoryService.DeleteFiles(expiredBackups.Select(f => f.FullName)); + } + logger.LogInformation("Finished cleanup of Database backups at {Time}", DateTime.Now); + } + + /// + /// Find any progress events that have duplicate, find the highest page read event, then copy over information from that and delete others, to leave one. + /// + public async Task ConsolidateProgress(CancellationToken ct = default) + { + logger.LogInformation("Consolidating Progress Events"); + // AppUserProgress + var allProgress = await unitOfWork.AppUserProgressRepository.GetAllProgress(ct); + + // Group by the unique identifiers that would make a progress entry unique + var duplicateGroups = allProgress + .GroupBy(p => new + { + p.AppUserId, + p.ChapterId, + }) + .Where(g => g.Count() > 1); + + foreach (var group in duplicateGroups) + { + // Find the entry with the highest pages read + var highestProgress = group + .OrderByDescending(p => p.PagesRead) + .ThenByDescending(p => p.LastModifiedUtc) + .First(); + + // Get the duplicate entries to remove (all except the highest progress) + var duplicatesToRemove = group + .Where(p => p.Id != highestProgress.Id) + .ToList(); + + // Copy over any non-null BookScrollId if the highest progress entry doesn't have one + if (string.IsNullOrEmpty(highestProgress.BookScrollId)) + { + var firstValidScrollId = duplicatesToRemove + .FirstOrDefault(p => !string.IsNullOrEmpty(p.BookScrollId)) + ?.BookScrollId; + + if (firstValidScrollId != null) + { + highestProgress.BookScrollId = firstValidScrollId; + highestProgress.MarkModified(); + } + } + + // Remove the duplicates + foreach (var duplicate in duplicatesToRemove) + { + unitOfWork.AppUserProgressRepository.Remove(duplicate); + } + } + + // Save changes + await unitOfWork.CommitAsync(ct); + } + + /// + /// Scans through Media Error and removes any entries that have been fixed and are within the DB (proper files where wordcount/pagecount > 0) + /// + public async Task CleanupMediaErrors(CancellationToken ct = default) + { + try + { + List errorStrings = ["This archive cannot be read or not supported", "File format not supported"]; + var mediaErrors = await unitOfWork.MediaErrorRepository.GetAllErrorsAsync(errorStrings, ct); + logger.LogInformation("Beginning consolidation of {Count} Media Errors", mediaErrors.Count); + + var pathToErrorMap = mediaErrors + .GroupBy(me => Parser.NormalizePath(me.FilePath)) + .ToDictionary( + group => group.Key, + group => group.ToList() // The same file can be duplicated (rare issue when network drives die out midscan) + ); + + var normalizedPaths = pathToErrorMap.Keys.ToList(); + + // Find all files that are valid + var validFiles = await unitOfWork.DataContext.MangaFile + .Where(f => normalizedPaths.Contains(f.FilePath) && f.Pages > 0) + .Select(f => f.FilePath) + .ToListAsync(cancellationToken: ct); + + var removalCount = 0; + foreach (var validFilePath in validFiles) + { + if (!pathToErrorMap.TryGetValue(validFilePath, out var mediaError)) continue; + + unitOfWork.MediaErrorRepository.Remove(mediaError); + removalCount++; + } + + await unitOfWork.CommitAsync(ct); + + logger.LogInformation("Finished consolidation of {Count} Media Errors, Removed: {RemovalCount}", + mediaErrors.Count, removalCount); + } + catch (Exception ex) + { + logger.LogError(ex, "There was an exception consolidating media errors"); + } + } + + public async Task CleanupLogs(CancellationToken ct = default) + { + logger.LogInformation("Performing cleanup of logs directory"); + var dayThreshold = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct)).TotalLogs; + var deltaTime = DateTime.Today.Subtract(TimeSpan.FromDays(dayThreshold)); + var allLogs = directoryService.GetFiles(directoryService.LogDirectory).ToList(); + var expiredLogs = allLogs.Select(filename => directoryService.FileSystem.FileInfo.New(filename)) + .Where(f => f.CreationTime < deltaTime) + .ToList(); + + if (expiredLogs.Count == allLogs.Count) + { + logger.LogInformation("All expired backups are older than {Threshold} days. Removing all but last backup", dayThreshold); + var toDelete = expiredLogs.OrderBy(f => f.CreationTime).ToList(); + directoryService.DeleteFiles(toDelete.Take(toDelete.Count - 1).Select(f => f.FullName)); + } + else + { + directoryService.DeleteFiles(expiredLogs.Select(f => f.FullName)); + } + logger.LogInformation("Finished cleanup of logs at {Time}", DateTime.Now); + } + + public void CleanupTemp() + { + logger.LogInformation("Performing cleanup of Temp directory"); + directoryService.ExistOrCreate(directoryService.TempDirectory); + + try + { + directoryService.ClearDirectory(directoryService.TempDirectory); + } + catch (Exception ex) + { + logger.LogError(ex, "There was an issue deleting one or more folders/files during cleanup"); + } + + logger.LogInformation("Temp directory purged"); + } + + /// + /// Ensures that each chapter's progress (pages read) is capped at the total pages. This can get out of sync when a chapter is replaced after being read with one with lower page count. + /// + /// + public async Task EnsureChapterProgressIsCapped(CancellationToken ct = default) + { + logger.LogInformation("Cleaning up any progress rows that exceed chapter page count"); + await unitOfWork.AppUserProgressRepository.UpdateAllProgressThatAreMoreThanChapterPages(ct); + logger.LogInformation("Cleaning up any progress rows that exceed chapter page count - complete"); + } + + /// + /// This does not cleanup any Series that are not Completed or Cancelled + /// + public async Task CleanupWantToRead(CancellationToken ct = default) + { + logger.LogInformation("Performing cleanup of Series that are Completed and have been fully read that are in Want To Read list"); + + var libraryIds = (await unitOfWork.LibraryRepository.GetLibrariesAsync(ct: ct)).Select(l => l.Id).ToList(); + var filter = new FilterDto() + { + PublicationStatus = new List() + { + PublicationStatus.Completed, + PublicationStatus.Cancelled + }, + Libraries = libraryIds, + ReadStatus = new ReadStatus() + { + Read = true, + InProgress = false, + NotRead = false + } + }; + foreach (var user in await unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.WantToRead, ct: ct)) + { + var series = await unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(0, user.Id, new UserParams(), filter); + 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.SeriesId)).ToList(); + unitOfWork.UserRepository.Update(user); + } + + if (unitOfWork.HasChanges()) + { + await unitOfWork.CommitAsync(ct); + } + + logger.LogInformation("Performing cleanup of Series that are Completed and have been fully read that are in Want To Read list, completed"); + } +} diff --git a/API/Services/ClientDeviceService.cs b/Kavita.Services/ClientDeviceService.cs similarity index 78% rename from API/Services/ClientDeviceService.cs rename to Kavita.Services/ClientDeviceService.cs index f36289229..938fde728 100644 --- a/API/Services/ClientDeviceService.cs +++ b/Kavita.Services/ClientDeviceService.cs @@ -5,37 +5,20 @@ using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.Device.ClientDevice; -using API.DTOs.Progress; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Progress; -using API.Entities.User; -using API.Extensions.QueryExtensions; -using AutoMapper; -using AutoMapper.QueryableExtensions; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; using Kavita.Common.Extensions; +using Kavita.Models.DTOs.Device.ClientDevice; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Services; -#nullable enable +namespace Kavita.Services; -public interface IClientDeviceService -{ - Task IdentifyOrRegisterDeviceAsync(int userId, ClientInfoData clientInfo, string? uiFingerprint, CancellationToken cancellationToken = default); - Task> GetUserDevicesAsync(int userId, bool includeInactive = false); - Task> GetUserDeviceDtosAsync(int userId, bool includeInactive = false); - Task> GetAllUserDeviceDtos(bool includeInactive = false); - Task RenameDeviceAsync(int userId, int deviceId, string newName); - Task DeleteDeviceAsync(int userId, int deviceId); - Task UpdateFriendlyNameAsync(int userId, UpdateClientDeviceNameDto dto); -} - -public class ClientDeviceService(DataContext context, IMapper mapper, ILogger logger) +public class ClientDeviceService(IDataContext context, IUnitOfWork unitOfWork ,ILogger logger) : IClientDeviceService { /// @@ -52,7 +35,7 @@ public class ClientDeviceService(DataContext context, IMapper mapper, ILogger GetClientDeviceByClientFingerprint(int userId, string uiFingerprint, CancellationToken cancellationToken) + public async Task RenameDeviceAsync(int userId, int deviceId, string newName, CancellationToken ct = default) { - return await context.ClientDevice - .Include(d => d.History.OrderByDescending(h => h.CapturedAtUtc).Take(1)) - .FirstOrDefaultAsync(d => - d.AppUserId == userId && - d.UiFingerprint == uiFingerprint && - d.IsActive, cancellationToken: cancellationToken); - } - - public async Task> GetUserDevicesAsync(int userId, bool includeInactive = false) - { - return await context.ClientDevice - .Where(d => d.AppUserId == userId) - .WhereIf(!includeInactive, d => d.IsActive) - .OrderByDescending(d => d.LastSeenUtc) - .ToListAsync(); - } - - public async Task> GetUserDeviceDtosAsync(int userId, bool includeInactive = false) - { - return await context.ClientDevice - .Where(d => d.AppUserId == userId) - .WhereIf(!includeInactive, d => d.IsActive) - .OrderByDescending(d => d.LastSeenUtc) - .ProjectTo(mapper.ConfigurationProvider) - .ToListAsync(); - } - - public async Task> GetAllUserDeviceDtos(bool includeInactive = false) - { - return await context.ClientDevice - .WhereIf(!includeInactive, d => d.IsActive) - .OrderByDescending(d => d.LastSeenUtc) - .ProjectTo(mapper.ConfigurationProvider) - .ToListAsync(); - } - - public async Task RenameDeviceAsync(int userId, int deviceId, string newName) - { - var device = await context.ClientDevice - .FirstOrDefaultAsync(d => d.Id == deviceId && d.AppUserId == userId); + var device = await unitOfWork.ClientDeviceRepository.GetClientDeviceById(deviceId, userId, ct); if (device == null) { @@ -152,7 +96,7 @@ public class ClientDeviceService(DataContext context, IMapper mapper, ILogger DeleteDeviceAsync(int userId, int deviceId) + public async Task DeleteDeviceAsync(int userId, int deviceId, CancellationToken ct = default) { - var device = await context.ClientDevice - .FirstOrDefaultAsync(d => d.Id == deviceId && d.AppUserId == userId); + var device = await unitOfWork.ClientDeviceRepository.GetClientDeviceById(deviceId, userId, ct); if (device == null) { @@ -171,7 +114,7 @@ public class ClientDeviceService(DataContext context, IMapper mapper, ILogger d.AppUserId == userId && d.Id == dto.DeviceId) - .FirstOrDefaultAsync() ?? throw new KavitaException("client-device-doesnt-exist"); + var device = await unitOfWork.ClientDeviceRepository.GetClientDeviceById(dto.DeviceId, userId, ct) + ?? throw new KavitaException("client-device-doesnt-exist"); if (!string.IsNullOrWhiteSpace(dto.Name)) { device.FriendlyName = dto.Name; - await context.SaveChangesAsync(); + await unitOfWork.CommitAsync(ct); } } diff --git a/Kavita.Services/CollectionTagService.cs b/Kavita.Services/CollectionTagService.cs new file mode 100644 index 000000000..62dd9b71e --- /dev/null +++ b/Kavita.Services/CollectionTagService.cs @@ -0,0 +1,125 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; +using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Collection; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Models.Extensions; + +namespace Kavita.Services; + +public class CollectionTagService(IUnitOfWork unitOfWork, IEventHub eventHub, IDirectoryService directoryService) : ICollectionTagService +{ + public async Task DeleteTag(int tagId, AppUser user, CancellationToken ct = default) + { + var collectionTag = await unitOfWork.CollectionTagRepository.GetCollectionAsync(tagId, ct: ct); + if (collectionTag == null) return true; + + user.Collections.Remove(collectionTag); + + if (!unitOfWork.HasChanges()) return true; + + return await unitOfWork.CommitAsync(ct); + } + + + public async Task UpdateTag(AppUserCollectionDto dto, int userId, CancellationToken ct = default) + { + var existingTag = await unitOfWork.CollectionTagRepository.GetCollectionAsync(dto.Id, ct: ct); + if (existingTag == null) throw new KavitaException("collection-doesnt-exist"); + if (existingTag.AppUserId != userId) throw new KavitaException("access-denied"); + + var title = dto.Title.Trim(); + if (string.IsNullOrEmpty(title)) throw new KavitaException("collection-tag-title-required"); + + // Ensure the title doesn't exist on the user's account already + if (!title.Equals(existingTag.Title) && await unitOfWork.CollectionTagRepository.CollectionExists(dto.Title, userId, ct)) + throw new KavitaException("collection-tag-duplicate"); + + existingTag.Items ??= []; + if (existingTag.Source == ScrobbleProvider.Kavita) + { + existingTag.Title = title; + existingTag.NormalizedTitle = dto.Title.ToNormalized(); + } + + var roles = await unitOfWork.UserRepository.GetRoles(userId, ct); + if (roles.Contains(PolicyConstants.AdminRole) || roles.Contains(PolicyConstants.PromoteRole)) + { + existingTag.Promoted = dto.Promoted; + } + existingTag.CoverImageLocked = dto.CoverImageLocked; + unitOfWork.CollectionTagRepository.Update(existingTag); + + // Check if Tag has updated (Summary) + var summary = (dto.Summary ?? string.Empty).Trim(); + if (existingTag.Summary == null || !existingTag.Summary.Equals(summary)) + { + existingTag.Summary = summary; + unitOfWork.CollectionTagRepository.Update(existingTag); + } + + // If we unlock the cover image, it means reset + if (!dto.CoverImageLocked) + { + existingTag.CoverImageLocked = false; + existingTag.CoverImage = string.Empty; + await eventHub.SendMessageAsync(MessageFactory.CoverUpdate, + MessageFactory.CoverUpdateEvent(existingTag.Id, MessageFactoryEntityTypes.Collection), false, ct); + unitOfWork.CollectionTagRepository.Update(existingTag); + } + + if (!unitOfWork.HasChanges()) return true; + return await unitOfWork.CommitAsync(ct); + } + + public async Task RemoveTagFromSeries(AppUserCollection? tag, IEnumerable seriesIds, CancellationToken ct = default) + { + if (tag == null) return false; + + tag.Items ??= []; + tag.Items = tag.Items.Where(s => !seriesIds.Contains(s.Id)).ToList(); + + if (tag.Items.Count == 0) + { + unitOfWork.CollectionTagRepository.Remove(tag); + } + + if (!unitOfWork.HasChanges()) return true; + + var result = await unitOfWork.CommitAsync(ct); + if (tag.Items.Count > 0) + { + await unitOfWork.CollectionTagRepository.UpdateCollectionAgeRating(tag, ct); + } + + return result; + } + + public async Task GenerateCollectionCoverImage(int collectionId) + { + var covers = await unitOfWork.CollectionTagRepository.GetRandomCoverImagesAsync(collectionId); + var destFile = directoryService.FileSystem.Path.Join(directoryService.TempDirectory, ImageService.GetCollectionTagFormat(collectionId)); + + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + destFile += settings.EncodeMediaAs.GetExtension(); + + if (directoryService.FileSystem.File.Exists(destFile)) return destFile; + + ImageService.CreateMergedImage( + covers.Select(c => directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory, c)).ToList(), + settings.CoverImageSize, + destFile); + + // TODO: Refactor this so that collections have a dedicated cover image so we can calculate primary/secondary colors + return !directoryService.FileSystem.File.Exists(destFile) ? string.Empty : destFile; + } +} diff --git a/API/Comparators/ChapterSortComparer.cs b/Kavita.Services/Comparators/ChapterSortComparer.cs similarity index 94% rename from API/Comparators/ChapterSortComparer.cs rename to Kavita.Services/Comparators/ChapterSortComparer.cs index f5d566cb1..1b06399bc 100644 --- a/API/Comparators/ChapterSortComparer.cs +++ b/Kavita.Services/Comparators/ChapterSortComparer.cs @@ -1,10 +1,8 @@ -using System.Collections.Generic; -using API.Extensions; -using API.Services.Tasks.Scanner.Parser; +using System.Collections.Generic; +using Kavita.Common.Extensions; +using Parser = Kavita.Services.Scanner.Parser; -namespace API.Comparators; - -#nullable enable +namespace Kavita.Services.Comparators; /// /// Sorts chapters based on their Number. Uses natural ordering of doubles. Specials always LAST. diff --git a/Kavita.Services/DeviceService.cs b/Kavita.Services/DeviceService.cs new file mode 100644 index 000000000..3252149c5 --- /dev/null +++ b/Kavita.Services/DeviceService.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Reading; +using Kavita.Common; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.Device.EmailDevice; +using Kavita.Models.DTOs.Email; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.Device; +using Kavita.Models.Entities.User; +using Microsoft.Extensions.Logging; + +namespace Kavita.Services; + +public class DeviceService( + IUnitOfWork unitOfWork, + ILogger logger, + IEmailService emailService, + IReadingProfileService readingProfileService) + : IDeviceService +{ + public async Task Create(CreateEmailDeviceDto dto, AppUser userWithDevices, CancellationToken ct = default) + { + try + { + userWithDevices.Devices ??= new List(); + var existingDevice = userWithDevices.Devices.SingleOrDefault(d => d.Name!.Equals(dto.Name)); + if (existingDevice != null) throw new KavitaException("device-duplicate"); + + existingDevice = new DeviceBuilder(dto.Name) + .WithPlatform(dto.Platform) + .WithEmail(dto.EmailAddress) + .Build(); + + + userWithDevices.Devices.Add(existingDevice); + unitOfWork.UserRepository.Update(userWithDevices); + + if (!unitOfWork.HasChanges()) return existingDevice; + if (await unitOfWork.CommitAsync(ct)) return existingDevice; + } + catch (Exception ex) + { + logger.LogError(ex, "There was an error when creating your device"); + await unitOfWork.RollbackAsync(ct); + } + + return null; + } + + public async Task Update(UpdateEmailDeviceDto dto, AppUser userWithDevices, CancellationToken ct = default) + { + try + { + var existingDevice = userWithDevices.Devices.SingleOrDefault(d => d.Id == dto.Id); + if (existingDevice == null) throw new KavitaException("device-not-created"); + + existingDevice.Name = dto.Name; + existingDevice.Platform = dto.Platform; + existingDevice.EmailAddress = dto.EmailAddress; + + if (!unitOfWork.HasChanges()) return existingDevice; + if (await unitOfWork.CommitAsync(ct)) return existingDevice; + } + catch (Exception ex) + { + logger.LogError(ex, "There was an error when updating your device"); + await unitOfWork.RollbackAsync(ct); + } + + return null; + } + + public async Task Delete(AppUser userWithDevices, int deviceId, CancellationToken ct = default) + { + try + { + userWithDevices.Devices = userWithDevices.Devices.Where(d => d.Id != deviceId).ToList(); + unitOfWork.UserRepository.Update(userWithDevices); + + await readingProfileService.RemoveDeviceLinks(userWithDevices.Id, deviceId); + + if (!unitOfWork.HasChanges()) return true; + if (await unitOfWork.CommitAsync(ct)) return true; + } + catch (Exception ex) + { + logger.LogError(ex, "There was an issue with deleting the device, {DeviceId} for user {UserName}", deviceId, userWithDevices.UserName); + } + + return false; + } + + public async Task SendTo(IReadOnlyList chapterIds, int deviceId, CancellationToken ct = default) + { + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct); + if (!settings.IsEmailSetupForSendToDevice()) + throw new KavitaException("send-to-kavita-email"); + + var device = await unitOfWork.DeviceRepository.GetDeviceById(deviceId, ct); + if (device == null) throw new KavitaException("device-doesnt-exist"); + + var files = await unitOfWork.ChapterRepository.GetFilesForChaptersAsync(chapterIds, ct); + if (files.Any(f => f.Format is not (MangaFormat.Epub or MangaFormat.Pdf)) && device.Platform == EmailDevicePlatform.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"); + + + try + { + device.UpdateLastUsed(); + unitOfWork.DeviceRepository.Update(device); + await unitOfWork.CommitAsync(ct); + } + catch (Exception ex) + { + logger.LogError(ex, "There was an issue updating device last used time"); + } + + var success = await emailService.SendFilesToEmail(new SendToDto() + { + DestinationEmail = device.EmailAddress!, + FilePaths = files.Select(m => m.FilePath) + }); + + return success; + } +} diff --git a/API/Services/DeviceTrackingService.cs b/Kavita.Services/DeviceTrackingService.cs similarity index 84% rename from API/Services/DeviceTrackingService.cs rename to Kavita.Services/DeviceTrackingService.cs index b9f99c572..1b58742ce 100644 --- a/API/Services/DeviceTrackingService.cs +++ b/Kavita.Services/DeviceTrackingService.cs @@ -2,23 +2,16 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.Entities.Progress; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.Models.Entities.Progress; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Logging; -namespace API.Services; -#nullable enable +namespace Kavita.Services; -public interface IDeviceTrackingService -{ - Task TrackDeviceAsync(int userId, ClientInfoData clientInfo, string? uiFingerprint, CancellationToken ct); - Task ClearDeviceCacheAsync(int deviceId); - Task ClearUserDeviceCachesAsync(int userId); -} - -public class DeviceTrackingService(HybridCache cache, DataContext context, ILogger logger, IClientDeviceService clientDeviceService) : IDeviceTrackingService +public class DeviceTrackingService(HybridCache cache, IDataContext context, ILogger logger, IClientDeviceService clientDeviceService) : IDeviceTrackingService { private static readonly HybridCacheEntryOptions CacheOptions = new() diff --git a/API/Services/DirectoryService.cs b/Kavita.Services/DirectoryService.cs similarity index 88% rename from API/Services/DirectoryService.cs rename to Kavita.Services/DirectoryService.cs index ecce1957a..61e74da9c 100644 --- a/API/Services/DirectoryService.cs +++ b/Kavita.Services/DirectoryService.cs @@ -6,84 +6,15 @@ using System.IO.Abstractions; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; -using API.DTOs.System; -using API.Entities.Enums; -using API.Extensions; -using API.Services.Tasks.Scanner.Parser; +using Kavita.API.Services; +using Kavita.Common.Extensions; using Kavita.Common.Helpers; +using Kavita.Models.DTOs.System; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; -namespace API.Services; -#nullable enable +namespace Kavita.Services; -public interface IDirectoryService -{ - IFileSystem FileSystem { get; } - string CacheDirectory { get; } - string CoverImageDirectory { get; } - string LogDirectory { get; } - string TempDirectory { get; } - string ConfigDirectory { get; } - string SiteThemeDirectory { get; } - string FaviconDirectory { get; } - string LocalizationDirectory { get; } - string CustomizedTemplateDirectory { get; } - string TemplateDirectory { get; } - string PublisherDirectory { get; } - /// - /// Used for caching documents that may need to stay on disk for more than a day - /// - string LongTermCacheDirectory { get; } - /// - /// Original BookmarkDirectory. Only used for resetting directory. Use for actual path. - /// - string BookmarkDirectory { get; } - /// - /// Used for random files needed, like images to check against, list of countries, etc - /// - string AssetsDirectory { get; } - string EpubFontDirectory { get; } - /// - /// Lists out top-level folders for a given directory. Filters out System and Hidden folders. - /// - /// Absolute path of directory to scan. - /// List of folder names - IEnumerable ListDirectory(string rootPath); - Task ReadFileAsync(string path); - bool CopyFilesToDirectory(IEnumerable filePaths, string directoryPath, string prepend = ""); - bool CopyFilesToDirectory(IEnumerable filePaths, string directoryPath, IList newFilenames); - bool Exists(string directory); - void CopyFileToDirectory(string fullFilePath, string targetDirectory); - int TraverseTreeParallelForEach(string root, Action action, string searchPattern, ILogger logger); - bool IsDriveMounted(string path); - bool IsDirectoryEmpty(string path); - long GetTotalSize(IEnumerable paths); - void ClearDirectory(string directoryPath); - void ClearAndDeleteDirectory(string directoryPath); - string[] GetFilesWithExtension(string path, string searchPatternExpression = ""); - bool CopyDirectoryToDirectory(string? sourceDirName, string destDirName, string searchPattern = ""); - Dictionary FindHighestDirectoriesFromFiles(IEnumerable libraryFolders, - IList filePaths); - string? FindLowestDirectoriesFromFiles(IList libraryFolders, - IList filePaths); - IEnumerable GetFoldersTillRoot(string rootPath, string fullPath); - IEnumerable GetFiles(string path, string fileNameRegex = "", SearchOption searchOption = SearchOption.TopDirectoryOnly); - bool ExistOrCreate(string directoryPath); - void DeleteFiles(IEnumerable files); - void CopyFile(string sourcePath, string destinationPath, bool overwrite = true); - void RemoveNonImages(string directoryName); - void Flatten(string directoryName); - Task CheckWriteAccess(string directoryName); - IEnumerable GetFilesWithCertainExtensions(string path, - string searchPatternExpression = "", - SearchOption searchOption = SearchOption.TopDirectoryOnly); - IEnumerable GetDirectories(string folderPath); - IEnumerable GetDirectories(string folderPath, GlobMatcher? matcher); - IEnumerable GetAllDirectories(string folderPath, GlobMatcher? matcher = null); - string GetParentDirectoryName(string fileOrFolder); - IList ScanFiles(string folderPath, string fileTypes, GlobMatcher? matcher = null, SearchOption searchOption = SearchOption.AllDirectories); - DateTime GetLastWriteTime(string folderPath); -} public class DirectoryService : IDirectoryService { public IFileSystem FileSystem { get; } @@ -102,6 +33,7 @@ public class DirectoryService : IDirectoryService public string PublisherDirectory { get; } public string LongTermCacheDirectory { get; } public string EpubFontDirectory { get; } + public string BackupDirectory { get; } private readonly ILogger _logger; private const RegexOptions MatchOptions = RegexOptions.Compiled | RegexOptions.IgnoreCase; @@ -111,7 +43,6 @@ public class DirectoryService : IDirectoryService MatchOptions, Parser.RegexTimeout); private static readonly Regex FileCopyAppend = new Regex(@"\(\d+\)", MatchOptions, Parser.RegexTimeout); - public static readonly string BackupDirectory = Path.Join(Directory.GetCurrentDirectory(), "config", "backups"); public DirectoryService(ILogger logger, IFileSystem fileSystem) { @@ -146,12 +77,14 @@ public class DirectoryService : IDirectoryService ExistOrCreate(LongTermCacheDirectory); EpubFontDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "fonts"); ExistOrCreate(EpubFontDirectory); + BackupDirectory = FileSystem.Path.Join(Directory.GetCurrentDirectory(), "config", "backups"); + ExistOrCreate(BackupDirectory); } /// /// Given a set of regex search criteria, get files in the given path. /// - /// This will always exclude patterns + /// This will always exclude patterns /// Directory to search /// Regex version of search pattern (e.g., \.mp3|\.mp4). Defaults to * meaning all files. /// SearchOption to use, defaults to TopDirectoryOnly @@ -253,7 +186,7 @@ public class DirectoryService : IDirectoryService if (!string.IsNullOrEmpty(fileNameRegex)) { // Compile the regex for better performance when used frequently - reSearchPattern = new Regex(fileNameRegex, RegexOptions.IgnoreCase | RegexOptions.Compiled, Tasks.Scanner.Parser.Parser.RegexTimeout); + reSearchPattern = new Regex(fileNameRegex, RegexOptions.IgnoreCase | RegexOptions.Compiled, Parser.RegexTimeout); } // Enumerate files lazily @@ -262,7 +195,7 @@ public class DirectoryService : IDirectoryService var fileName = FileSystem.Path.GetFileName(file); // Exclude macOS metadata files - if (fileName.StartsWith(Tasks.Scanner.Parser.Parser.MacOsMetadataFileStartsWith)) + if (fileName.StartsWith(Parser.MacOsMetadataFileStartsWith)) continue; // If a regex is provided, match the file name against it @@ -609,10 +542,10 @@ public class DirectoryService : IDirectoryService { var stopLookingForDirectories = false; var dirs = new Dictionary(); - foreach (var folder in libraryFolders.Select(Tasks.Scanner.Parser.Parser.NormalizePath)) + foreach (var folder in libraryFolders.Select(Parser.NormalizePath)) { if (stopLookingForDirectories) break; - foreach (var file in filePaths.Select(Tasks.Scanner.Parser.Parser.NormalizePath)) + foreach (var file in filePaths.Select(Parser.NormalizePath)) { if (!file.Contains(folder)) continue; @@ -625,7 +558,7 @@ public class DirectoryService : IDirectoryService break; } - var fullPath = Tasks.Scanner.Parser.Parser.NormalizePath(Path.Join(folder, parts[parts.Count - 1])); + var fullPath =Parser.NormalizePath(Path.Join(folder, parts[parts.Count - 1])); dirs.TryAdd(fullPath, string.Empty); } } @@ -747,7 +680,7 @@ public class DirectoryService : IDirectoryService { try { - return Tasks.Scanner.Parser.Parser.NormalizePath(Directory.GetParent(fileOrFolder)?.FullName); + return Parser.NormalizePath(Directory.GetParent(fileOrFolder)?.FullName); } catch (Exception) { @@ -1025,7 +958,7 @@ public class DirectoryService : IDirectoryService /// Fully qualified directory public void RemoveNonImages(string directoryName) { - DeleteFiles(GetFiles(directoryName, searchOption:SearchOption.AllDirectories).Where(file => !Tasks.Scanner.Parser.Parser.IsImage(file))); + DeleteFiles(GetFiles(directoryName, searchOption:SearchOption.AllDirectories).Where(file => !Parser.IsImage(file))); } @@ -1098,9 +1031,9 @@ public class DirectoryService : IDirectoryService foreach (var file in directory.EnumerateFiles().OrderByNatural(file => file.FullName)) { if (file.Directory == null) continue; - var paddedIndex = Tasks.Scanner.Parser.Parser.PadZeros(directoryIndex + string.Empty); + var paddedIndex =Parser.PadZeros(directoryIndex + string.Empty); // We need to rename the files so that after flattening, they are in the order we found them - var newName = $"{paddedIndex}_{Tasks.Scanner.Parser.Parser.PadZeros(fileIndex + string.Empty)}{file.Extension}"; + var newName = $"{paddedIndex}_{Parser.PadZeros(fileIndex + string.Empty)}{file.Extension}"; var newPath = Path.Join(root.FullName, newName); if (!File.Exists(newPath)) file.MoveTo(newPath); fileIndex++; @@ -1112,7 +1045,7 @@ public class DirectoryService : IDirectoryService foreach (var subDirectory in directory.EnumerateDirectories().OrderByNatural(d => d.FullName)) { // We need to check if the directory is not a blacklisted (ie __MACOSX) - if (Tasks.Scanner.Parser.Parser.HasBlacklistedFolderInPath(subDirectory.FullName)) continue; + if (Parser.HasBlacklistedFolderInPath(subDirectory.FullName)) continue; FlattenDirectory(root, subDirectory, ref directoryIndex); } diff --git a/API/Services/DownloadService.cs b/Kavita.Services/DownloadService.cs similarity index 89% rename from API/Services/DownloadService.cs rename to Kavita.Services/DownloadService.cs index b3913e8be..4ca185c08 100644 --- a/API/Services/DownloadService.cs +++ b/Kavita.Services/DownloadService.cs @@ -2,17 +2,13 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using API.Entities; +using Kavita.API.Services; +using Kavita.Models.Entities; using Microsoft.AspNetCore.StaticFiles; using MimeTypes; -namespace API.Services; +namespace Kavita.Services; -public interface IDownloadService -{ - Tuple GetFirstFileDownload(IEnumerable files); - string GetContentTypeFromFile(string filepath); -} public class DownloadService : IDownloadService { private readonly FileExtensionContentTypeProvider _fileTypeProvider = new FileExtensionContentTypeProvider(); diff --git a/API/Services/EmailService.cs b/Kavita.Services/EmailService.cs similarity index 81% rename from API/Services/EmailService.cs rename to Kavita.Services/EmailService.cs index f2988a342..215a7f3c8 100644 --- a/API/Services/EmailService.cs +++ b/Kavita.Services/EmailService.cs @@ -7,15 +7,15 @@ using System.Net; using System.Text; using System.Threading.Tasks; using System.Web; -using API.Data; -using API.DTOs.Account; -using API.DTOs.Email; -using API.Entities; -using API.Entities.User; -using API.Services.Plus; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; using Kavita.Common.EnvironmentInfo; using Kavita.Common.Extensions; +using Kavita.Models.DTOs.Email; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; using MailKit.Security; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Hosting; @@ -23,8 +23,7 @@ using Microsoft.Extensions.Logging; using MimeKit; using MimeTypes; -namespace API.Services; -#nullable enable +namespace Kavita.Services; internal class EmailOptionsDto { @@ -41,33 +40,14 @@ internal class EmailOptionsDto public required string Template { get; set; } } -public interface IEmailService +public class EmailService( + ILogger logger, + IUnitOfWork unitOfWork, + IDirectoryService directoryService, + IHostEnvironment environment, + ILocalizationService localizationService) + : IEmailService { - Task SendInviteEmail(ConfirmationEmailDto data); - Task SendForgotPasswordEmail(PasswordResetEmailDto dto); - Task SendFilesToEmail(SendToDto data); - Task SendTestEmail(string adminEmail); - Task SendEmailChangeEmail(ConfirmationEmailDto data); - bool IsValidEmail(string email); - - Task GenerateEmailLink(HttpRequest request, string token, string routePart, string email, - bool withHost = true); - - Task SendTokenExpiredEmail(int userId, ScrobbleProvider provider); - Task SendTokenExpiringSoonEmail(int userId, ScrobbleProvider provider); - Task SendAuthKeyExpiredEmail(int userId, IList keys); - Task SendAuthKeyExpiringSoonEmail(int userId, IList keys); - Task SendKavitaPlusDebug(); -} - -public class EmailService : IEmailService -{ - private readonly ILogger _logger; - private readonly IUnitOfWork _unitOfWork; - private readonly IDirectoryService _directoryService; - private readonly IHostEnvironment _environment; - private readonly ILocalizationService _localizationService; - private const string TemplatePath = @"{0}.html"; private const string LocalHost = "localhost:4200"; @@ -85,16 +65,6 @@ public class EmailService : IEmailService private const string AuthKeyExpiringFragment = "AuthKeyExpiringFragment"; private const string AuthKeyExpiredFragment = "AuthKeyExpiredFragment"; - public EmailService(ILogger logger, IUnitOfWork unitOfWork, IDirectoryService directoryService, - IHostEnvironment environment, ILocalizationService localizationService) - { - _logger = logger; - _unitOfWork = unitOfWork; - _directoryService = directoryService; - _environment = environment; - _localizationService = localizationService; - } - /// /// Test if the email settings are working. Rejects if user email isn't valid or not all data is setup in server settings. /// @@ -106,19 +76,19 @@ public class EmailService : IEmailService EmailAddress = adminEmail }; - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); if (!IsValidEmail(adminEmail)) { - var defaultAdmin = await _unitOfWork.UserRepository.GetDefaultAdminUser(); - result.ErrorMessage = await _localizationService.Translate(defaultAdmin.Id, "account-email-invalid"); + var defaultAdmin = await unitOfWork.UserRepository.GetDefaultAdminUser(); + result.ErrorMessage = await localizationService.Translate(defaultAdmin.Id, "account-email-invalid"); result.Successful = false; return result; } if (!settings.IsEmailSetup()) { - var defaultAdmin = await _unitOfWork.UserRepository.GetDefaultAdminUser(); - result.ErrorMessage = await _localizationService.Translate(defaultAdmin.Id, "email-settings-invalid"); + var defaultAdmin = await unitOfWork.UserRepository.GetDefaultAdminUser(); + result.ErrorMessage = await localizationService.Translate(defaultAdmin.Id, "email-settings-invalid"); result.Successful = false; return result; } @@ -193,8 +163,8 @@ public class EmailService : IEmailService 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 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)) { @@ -213,8 +183,8 @@ public class EmailService : IEmailService public async Task SendTokenExpiredEmail(int userId, ScrobbleProvider provider) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); if (user == null || !IsValidEmail(user.Email) || !settings.IsEmailSetup()) return false; var placeholders = new List> @@ -243,8 +213,8 @@ public class EmailService : IEmailService public async Task SendTokenExpiringSoonEmail(int userId, ScrobbleProvider provider) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); if (user == null || !IsValidEmail(user.Email) || !settings.IsEmailSetup()) return false; var placeholders = new List> @@ -273,8 +243,8 @@ public class EmailService : IEmailService public async Task SendAuthKeyExpiredEmail(int userId, IList keys) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); if (user == null || !IsValidEmail(user.Email) || !settings.IsEmailSetup()) return false; var d = keys.Select(k => new List>() @@ -309,8 +279,8 @@ public class EmailService : IEmailService public async Task SendAuthKeyExpiringSoonEmail(int userId, IList keys) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); if (user == null || !IsValidEmail(user.Email) || !settings.IsEmailSetup()) return false; var d = keys.Select(k => new List>() @@ -351,7 +321,7 @@ public class EmailService : IEmailService /// public async Task SendKavitaPlusDebug() { - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); if (!settings.IsEmailSetup()) return false; var placeholders = new List> @@ -431,7 +401,7 @@ public class EmailService : IEmailService public async Task SendFilesToEmail(SendToDto data) { - var serverSetting = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var serverSetting = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); if (!serverSetting.IsEmailSetupForSendToDevice()) return false; var emailOptions = new EmailOptionsDto() @@ -450,7 +420,7 @@ public class EmailService : IEmailService private async Task SendEmail(EmailOptionsDto userEmailOptions) { - var smtpConfig = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).SmtpConfig; + var smtpConfig = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).SmtpConfig; var email = new MimeMessage() { Subject = userEmailOptions.Subject, @@ -508,11 +478,11 @@ public class EmailService : IEmailService AppUser? user; if (userEmailOptions.Template == SendToDeviceTemplate) { - user = await _unitOfWork.UserRepository.GetUserByDeviceEmail(emailAddress); + user = await unitOfWork.UserRepository.GetUserByDeviceEmail(emailAddress); } else { - user = await _unitOfWork.UserRepository.GetUserByEmailAsync(emailAddress); + user = await unitOfWork.UserRepository.GetUserByEmailAsync(emailAddress); } @@ -532,13 +502,13 @@ public class EmailService : IEmailService } catch (Exception ex) { - _logger.LogError(ex, "There was an issue sending the email"); + logger.LogError(ex, "There was an issue sending the email"); if (user != null) { await LogEmailHistory(user.Id, userEmailOptions.Template, userEmailOptions.Subject, userEmailOptions.Body, "Failed", ex.Message); } - _logger.LogError("Could not find user on file for email, {Template} email was not sent and not recorded into history table", userEmailOptions.Template); + logger.LogError("Could not find user on file for email, {Template} email was not sent and not recorded into history table", userEmailOptions.Template); throw; } @@ -566,21 +536,21 @@ public class EmailService : IEmailService ErrorMessage = errorMessage }; - _unitOfWork.DataContext.EmailHistory.Add(emailHistory); - await _unitOfWork.CommitAsync(); + unitOfWork.DataContext.EmailHistory.Add(emailHistory); + await unitOfWork.CommitAsync(); } private async Task GetTemplatePath(string templateName) { - if ((await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).SmtpConfig.CustomizedTemplates) + if ((await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).SmtpConfig.CustomizedTemplates) { - var templateDirectory = Path.Join(_directoryService.CustomizedTemplateDirectory, TemplatePath); + 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 (directoryService.FileSystem.File.Exists(fullName)) return fullName; + logger.LogError("Customized Templates is on, but template {TemplatePath} is missing", fullName); } - return string.Format(Path.Join(_directoryService.TemplateDirectory, TemplatePath), templateName); + return string.Format(Path.Join(directoryService.TemplateDirectory, TemplatePath), templateName); } private async Task GetEmailBody(string templateName) diff --git a/API/Services/EntityNamingService.cs b/Kavita.Services/EntityNamingService.cs similarity index 85% rename from API/Services/EntityNamingService.cs rename to Kavita.Services/EntityNamingService.cs index ead40e7d3..85c4b92d0 100644 --- a/API/Services/EntityNamingService.cs +++ b/Kavita.Services/EntityNamingService.cs @@ -2,55 +2,14 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; -using API.DTOs; -using API.DTOs.ReadingLists; -using API.Entities.Enums; -using API.Services.Tasks.Scanner.Parser; +using Kavita.API.Services; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Extensions; +using Kavita.Services.Scanner; -namespace API.Services; -#nullable enable - -/// -/// Provides consistent, testable naming for series, volumes, and chapters across the application. -/// All methods are pure functions with no side effects. -/// -public interface IEntityNamingService -{ - /// - /// Formats a chapter title based on library type and chapter metadata. - /// - string FormatChapterTitle(LibraryType libraryType, ChapterDto chapter, string? chapterLabel = null, string? issueLabel = null, string? bookLabel = null); - - /// - /// Formats a chapter title from raw values. - /// - string FormatChapterTitle(LibraryType libraryType, bool isSpecial, string range, string? title, string? chapterLabel = null, string? issueLabel = null, string? bookLabel = null, bool withHash = true); - - /// - /// Formats a volume name based on library type and volume metadata. - /// - string? FormatVolumeName(LibraryType libraryType, VolumeDto volume, string? volumeLabel = null); - /// - /// Builds a full display title for a chapter within a series/volume context. - /// Used for OPDS feeds, reading lists, etc. - /// - string BuildFullTitle(LibraryType libraryType, SeriesDto series, VolumeDto? volume, ChapterDto chapter, string? volumeLabel = null, string? chapterLabel = null, string? issueLabel = null, string? bookLabel = null); - /// - /// Builds a display title for a chapter within its volume context. - /// Used when series context is not needed (e.g., reading history within a series grouping). - /// - string BuildChapterTitle(LibraryType libraryType, VolumeDto volume, ChapterDto chapter, string? volumeLabel = null, string? chapterLabel = null, string? issueLabel = null, string? bookLabel = null); - /// - /// Formats a reading list item title based on the item's metadata. - /// Handles the unique naming conventions for reading list display. - /// - string FormatReadingListItemTitle(ReadingListItemDto item, string? volumeLabel = null, string? chapterLabel = null, string? issueLabel = null, string? bookLabel = null); - - /// - /// Formats a reading list item title from raw values. - /// - string FormatReadingListItemTitle( LibraryType libraryType, MangaFormat format, string? chapterNumber, string? volumeNumber, string? chapterTitleName, bool isSpecial, string? volumeLabel = null, string? chapterLabel = null, string? issueLabel = null, string? bookLabel = null); -} +namespace Kavita.Services; public partial class EntityNamingService : IEntityNamingService { diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/Kavita.Services/Extensions/ApplicationServiceExtensions.cs similarity index 56% rename from API/Extensions/ApplicationServiceExtensions.cs rename to Kavita.Services/Extensions/ApplicationServiceExtensions.cs index 67d59022e..38f7c4001 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/Kavita.Services/Extensions/ApplicationServiceExtensions.cs @@ -1,41 +1,27 @@ -using System.IO.Abstractions; -using API.Constants; -using API.Data; -using API.Data.AutoMapper; -using API.Helpers; -using API.Services; -using API.Services.Caching; -using API.Services.Plus; -using API.Services.Reading; -using API.Services.Store; -using API.Services.Tasks; -using API.Services.Tasks.Metadata; -using API.Services.Tasks.Scanner; -using API.SignalR; -using API.SignalR.Presence; -using Kavita.Common; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Hosting; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Diagnostics; -using Microsoft.Extensions.Configuration; +using System.IO.Abstractions; +using Kavita.API.Services; +using Kavita.API.Services.Helpers; +using Kavita.API.Services.Metadata; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +using Kavita.API.Services.Scanner; +using Kavita.API.Services.SignalR; +using Kavita.Services.Helpers; +using Kavita.Services.HostedServices; +using Kavita.Services.Metadata; +using Kavita.Services.Plus; +using Kavita.Services.Reading; +using Kavita.Services.Scanner; +using Kavita.Services.SignalR; using Microsoft.Extensions.DependencyInjection; -using NeoSmart.Caching.Sqlite; - -namespace API.Extensions; +namespace Kavita.Services.Extensions; public static class ApplicationServiceExtensions { - public static void AddApplicationServices(this IServiceCollection services, IConfiguration config, IWebHostEnvironment env) + + public static void AddKavitaServices(this IServiceCollection services) { - services.AddAutoMapper(typeof(Program).Assembly); - - services.AddScoped(); - services.AddScoped(sp => sp.GetRequiredService()); - - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -87,7 +73,6 @@ public static class ApplicationServiceExtensions services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); @@ -107,56 +92,9 @@ public static class ApplicationServiceExtensions services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); - services.AddSqLite(); - services.AddSignalR(opt => opt.EnableDetailedErrors = true); - - services.AddEasyCaching(options => - { - options.UseInMemory(EasyCacheProfiles.Favicon); - options.UseInMemory(EasyCacheProfiles.Publisher); - options.UseInMemory(EasyCacheProfiles.Library); - options.UseInMemory(EasyCacheProfiles.RevokedJwt); - options.UseInMemory(EasyCacheProfiles.LocaleOptions); - - // KavitaPlus stuff - options.UseInMemory(EasyCacheProfiles.KavitaPlusExternalSeries); - options.UseInMemory(EasyCacheProfiles.License); - options.UseInMemory(EasyCacheProfiles.LicenseInfo); - options.UseInMemory(EasyCacheProfiles.KavitaPlusMatchSeries); - }); - - services.AddMemoryCache(options => - { - options.SizeLimit = Configuration.CacheSize * 1024 * 1024; // 75 MB - options.CompactionPercentage = 0.1; // LRU compaction, Evict 10% when limit reached - }); - - services.AddSingleton(); - services.AddSingleton(); - - services.AddSwaggerGen(g => - { - g.UseInlineDefinitionsForEnums(); - }); + services.AddHostedService(); } - private static void AddSqLite(this IServiceCollection services) - { - services.AddSqliteCache("config/cache.db"); - - services.AddDbContextPool(options => - { - options.UseSqlite("Data source=config/kavita.db", builder => - { - builder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); - }); - options.EnableDetailedErrors(); - options.EnableSensitiveDataLogging(); - options.ConfigureWarnings(warnings => - warnings.Ignore(RelationalEventId.PendingModelChangesWarning)); - }); - } } diff --git a/Kavita.Services/Extensions/ChapterExtensions.cs b/Kavita.Services/Extensions/ChapterExtensions.cs new file mode 100644 index 000000000..1323c1183 --- /dev/null +++ b/Kavita.Services/Extensions/ChapterExtensions.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Kavita.Common.Extensions; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Parser; + +namespace Kavita.Services.Extensions; + +public static class ChapterExtensions +{ + extension(Chapter chapter) + { + public void UpdateFrom(ParserInfo info) + { + chapter.Files ??= new List(); + chapter.IsSpecial = info.IsSpecialInfo(); + if (chapter.IsSpecial) + { + chapter.Number = Scanner.Parser.DefaultChapter; + chapter.MinNumber = Scanner.Parser.DefaultChapterNumber; + chapter.MaxNumber = Scanner.Parser.DefaultChapterNumber; + } + chapter.Title = (chapter.IsSpecial && info.Format is MangaFormat.Epub or MangaFormat.Pdf) + ? info.Title + : Scanner.Parser.RemoveExtensionIfSupported(chapter.Range); + + var specialTreatment = info.IsSpecialInfo(); + chapter.Range = specialTreatment ? info.Filename : info.Chapters; + } + + /// + /// Returns the Chapter Number. If the chapter is a range, returns that, formatted. + /// + /// + public string GetNumberTitle() + { + try + { + if (chapter.MinNumber.Is(chapter.MaxNumber)) + { + if (chapter.MinNumber.Is(Scanner.Parser.DefaultChapterNumber) && chapter.IsSpecial) + { + return Scanner.Parser.RemoveExtensionIfSupported(chapter.Title) ?? string.Empty; + } + + if (chapter.MinNumber.Is(0f) && !float.TryParse(chapter.Range, CultureInfo.InvariantCulture, out _)) + { + return $"{chapter.Range.ToString(CultureInfo.InvariantCulture)}"; + } + + return $"{chapter.MinNumber.ToString(CultureInfo.InvariantCulture)}"; + + } + + return $"{chapter.MinNumber.ToString(CultureInfo.InvariantCulture)}-{chapter.MaxNumber.ToString(CultureInfo.InvariantCulture)}"; + } + catch (Exception) + { + return chapter.MinNumber.ToString(CultureInfo.InvariantCulture); + } + } + } +} diff --git a/API/Extensions/ChapterListExtensions.cs b/Kavita.Services/Extensions/ChapterListExtensions.cs similarity index 80% rename from API/Extensions/ChapterListExtensions.cs rename to Kavita.Services/Extensions/ChapterListExtensions.cs index 0b6db61dc..0de71ea28 100644 --- a/API/Extensions/ChapterListExtensions.cs +++ b/Kavita.Services/Extensions/ChapterListExtensions.cs @@ -1,12 +1,11 @@ using System.Collections.Generic; using System.Linq; -using API.Entities; -using API.Helpers; -using API.Helpers.Builders; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Common.Helpers; +using Kavita.Models.Entities; +using Kavita.Models.Parser; +using Kavita.Services.Builders; -namespace API.Extensions; -#nullable enable +namespace Kavita.Services.Extensions; public static class ChapterListExtensions { @@ -30,13 +29,17 @@ public static class ChapterListExtensions /// public static Chapter? GetChapterByRange(this IEnumerable chapters, ParserInfo info) { - var normalizedPath = Parser.NormalizePath(info.FullFilePath); + var normalizedPath = Scanner.Parser.NormalizePath(info.FullFilePath); var specialTreatment = info.IsSpecialInfo(); + // NOTE: This can fail to find the chapter when Range is "1.0" as the chapter will store it as "1" hence why we need to emulate a Chapter var fakeChapter = new ChapterBuilder(info.Chapters, info.Chapters).Build(); fakeChapter.UpdateFrom(info); + return specialTreatment - ? chapters.FirstOrDefault(c => c.Range == Parser.RemoveExtensionIfSupported(info.Filename) || c.Files.Select(f => Parser.NormalizePath(f.FilePath)).Contains(normalizedPath)) + ? chapters.FirstOrDefault(c => + c.Range == Scanner.Parser.RemoveExtensionIfSupported(info.Filename) + || c.Files.Select(f => Scanner.Parser.NormalizePath(f.FilePath)).Contains(normalizedPath)) : chapters.FirstOrDefault(c => c.Range == fakeChapter.GetNumberTitle()); } diff --git a/Kavita.Services/Extensions/ComicInfoExtensions.cs b/Kavita.Services/Extensions/ComicInfoExtensions.cs new file mode 100644 index 000000000..1fc2e5de7 --- /dev/null +++ b/Kavita.Services/Extensions/ComicInfoExtensions.cs @@ -0,0 +1,77 @@ +using Kavita.Models.Metadata; +using Nager.ArticleNumber; + +namespace Kavita.Services.Extensions; + +public static class ComicInfoExtensions +{ + + extension(ComicInfo? info) + { + public void CleanComicInfo() + { + if (info == null) return; + + info.Series = info.Series.Trim(); + info.SeriesSort = info.SeriesSort.Trim(); + info.LocalizedSeries = info.LocalizedSeries.Trim(); + + info.Writer = Scanner.Parser.CleanAuthor(info.Writer); + info.Colorist = Scanner.Parser.CleanAuthor(info.Colorist); + info.Editor = Scanner.Parser.CleanAuthor(info.Editor); + info.Inker = Scanner.Parser.CleanAuthor(info.Inker); + info.Letterer = Scanner.Parser.CleanAuthor(info.Letterer); + info.Penciller = Scanner.Parser.CleanAuthor(info.Penciller); + info.Publisher = Scanner.Parser.CleanAuthor(info.Publisher); + info.Imprint = Scanner.Parser.CleanAuthor(info.Imprint); + info.Characters = Scanner.Parser.CleanAuthor(info.Characters); + info.Translator = Scanner.Parser.CleanAuthor(info.Translator); + info.CoverArtist = Scanner.Parser.CleanAuthor(info.CoverArtist); + info.Teams = Scanner.Parser.CleanAuthor(info.Teams); + info.Locations = Scanner.Parser.CleanAuthor(info.Locations); + + // We need to convert GTIN to ISBN + info.Isbn = ParseGtin(info.GTIN); + + if (!string.IsNullOrEmpty(info.Number)) + { + info.Number = info.Number.Trim().Replace(",", "."); // Corrective measure for non English OSes + } + + if (!string.IsNullOrEmpty(info.Volume)) + { + info.Volume = info.Volume.Trim(); + } + } + } + + /// + /// For a given GTIN, attempts to parse out an ISBN and set the Isbn property. + /// + /// + /// + public static string ParseGtin(string? gtin) + { + if (string.IsNullOrEmpty(gtin)) return string.Empty; + + + // This is likely a valid ISBN + if (gtin[0] == '0') + { + var offset = gtin[1] == '-' ? 0 : 1; + var potentialIsbn = gtin[offset..]; + if (ArticleNumberHelper.IsValidIsbn13(potentialIsbn)) + { + return potentialIsbn; + } + } + + if (ArticleNumberHelper.IsValidIsbn10(gtin) || ArticleNumberHelper.IsValidIsbn13(gtin)) + { + return gtin; + } + + return string.Empty; + } + +} diff --git a/API/Extensions/FileTypeGroupExtensions.cs b/Kavita.Services/Extensions/FileTypeGroupExtensions.cs similarity index 58% rename from API/Extensions/FileTypeGroupExtensions.cs rename to Kavita.Services/Extensions/FileTypeGroupExtensions.cs index 24073f642..1b0db59f0 100644 --- a/API/Extensions/FileTypeGroupExtensions.cs +++ b/Kavita.Services/Extensions/FileTypeGroupExtensions.cs @@ -1,8 +1,7 @@ -using System; -using API.Entities.Enums; -using API.Services.Tasks.Scanner.Parser; +using System; +using Kavita.Models.Entities.Enums; -namespace API.Extensions; +namespace Kavita.Services.Extensions; public static class FileTypeGroupExtensions { @@ -11,13 +10,13 @@ public static class FileTypeGroupExtensions switch (fileTypeGroup) { case FileTypeGroup.Archive: - return Parser.ArchiveFileExtensions; + return Scanner.Parser.ArchiveFileExtensions; case FileTypeGroup.Epub: - return Parser.EpubFileExtension; + return Scanner.Parser.EpubFileExtension; case FileTypeGroup.Pdf: - return Parser.PdfFileExtension; + return Scanner.Parser.PdfFileExtension; case FileTypeGroup.Images: - return Parser.ImageFileExtensions; + return Scanner.Parser.ImageFileExtensions; default: throw new ArgumentOutOfRangeException(nameof(fileTypeGroup), fileTypeGroup, null); } diff --git a/API/Extensions/IHasKPlusMetadataExtensions.cs b/Kavita.Services/Extensions/IHasKPlusMetadataExtensions.cs similarity index 79% rename from API/Extensions/IHasKPlusMetadataExtensions.cs rename to Kavita.Services/Extensions/IHasKPlusMetadataExtensions.cs index 84e35adc4..0e2a57164 100644 --- a/API/Extensions/IHasKPlusMetadataExtensions.cs +++ b/Kavita.Services/Extensions/IHasKPlusMetadataExtensions.cs @@ -1,7 +1,7 @@ -using API.Entities.Interfaces; -using API.Entities.MetadataMatching; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.MetadataMatching; -namespace API.Extensions; +namespace Kavita.Services.Extensions; public static class IHasKPlusMetadataExtensions { diff --git a/Kavita.Services/Extensions/ParserExtensions.cs b/Kavita.Services/Extensions/ParserExtensions.cs new file mode 100644 index 000000000..1b094878f --- /dev/null +++ b/Kavita.Services/Extensions/ParserExtensions.cs @@ -0,0 +1,36 @@ +using Kavita.Models.Parser; + +namespace Kavita.Services.Extensions; + +public static class ParserExtensions +{ + + extension(ParserInfo info) + { + /// + /// Merges non-empty/null properties from info2 into this entity. + /// + /// This does not merge ComicInfo as they should always be the same + /// + public void Merge(ParserInfo? info2) + { + if (info2 == null) return; + info.Chapters = Scanner.Parser.IsDefaultChapter(info.Chapters) ? info2.Chapters: info.Chapters; + info.Volumes = Scanner.Parser.IsLooseLeafVolume(info.Volumes) ? info2.Volumes : info.Volumes; + info.Edition = string.IsNullOrEmpty(info.Edition) ? info2.Edition : info.Edition; + info.Title = string.IsNullOrEmpty(info.Title) ? info2.Title : info.Title; + info.Series = string.IsNullOrEmpty(info.Series) ? info2.Series : info.Series; + info.IsSpecial = info.IsSpecial || info2.IsSpecial; + } + + /// + /// If the ParserInfo has the IsSpecial tag or both volumes and chapters are default aka 0 + /// + /// + public bool IsSpecialInfo() + { + return info.IsSpecial || (Scanner.Parser.IsLooseLeafVolume(info.Volumes) && Scanner.Parser.IsDefaultChapter(info.Chapters)); + } + } + +} diff --git a/API/Extensions/ParserInfoListExtensions.cs b/Kavita.Services/Extensions/ParserInfoListExtensions.cs similarity index 73% rename from API/Extensions/ParserInfoListExtensions.cs rename to Kavita.Services/Extensions/ParserInfoListExtensions.cs index 38a8ecc30..740d1b446 100644 --- a/API/Extensions/ParserInfoListExtensions.cs +++ b/Kavita.Services/Extensions/ParserInfoListExtensions.cs @@ -1,10 +1,9 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using API.Entities; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Models.Entities; +using Kavita.Models.Parser; -namespace API.Extensions; -#nullable enable +namespace Kavita.Services.Extensions; public static class ParserInfoListExtensions { @@ -27,8 +26,8 @@ public static class ParserInfoListExtensions /// public static bool HasInfo(this IList infos, Chapter chapter) { - var chapterFiles = chapter.Files.Select(x => Parser.NormalizePath(x.FilePath)).ToList(); - var infoFiles = infos.Select(x => Parser.NormalizePath(x.FullFilePath)).ToList(); + var chapterFiles = chapter.Files.Select(x => Scanner.Parser.NormalizePath(x.FilePath)).ToList(); + var infoFiles = infos.Select(x => Scanner.Parser.NormalizePath(x.FullFilePath)).ToList(); return infoFiles.Intersect(chapterFiles).Any(); } diff --git a/API/Extensions/SeriesExtensions.cs b/Kavita.Services/Extensions/SeriesExtensions.cs similarity index 81% rename from API/Extensions/SeriesExtensions.cs rename to Kavita.Services/Extensions/SeriesExtensions.cs index 01ae718c7..78de92330 100644 --- a/API/Extensions/SeriesExtensions.cs +++ b/Kavita.Services/Extensions/SeriesExtensions.cs @@ -1,10 +1,9 @@ -using System.Linq; -using API.Comparators; -using API.Entities; -using API.Services.Tasks.Scanner.Parser; +using System.Linq; +using Kavita.Common.Extensions; +using Kavita.Models.Entities; +using Kavita.Services.Comparators; -namespace API.Extensions; -#nullable enable +namespace Kavita.Services.Extensions; public static class SeriesExtensions { @@ -23,7 +22,7 @@ public static class SeriesExtensions if (firstVolume == null) return null; // If first volume here is specials, move to the next as specials should almost always be last. - if (firstVolume.MinNumber.Is(Parser.SpecialVolumeNumber) && volumes.Count > 1) + if (firstVolume.MinNumber.Is(Scanner.Parser.SpecialVolumeNumber) && volumes.Count > 1) { firstVolume = volumes[1]; } @@ -44,16 +43,16 @@ public static class SeriesExtensions } // just volumes - if (volumes.TrueForAll(v => v.MinNumber.IsNot(Parser.LooseLeafVolumeNumber))) + if (volumes.TrueForAll(v => v.MinNumber.IsNot(Scanner.Parser.LooseLeafVolumeNumber))) { 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].MinNumber.IsNot(Parser.LooseLeafVolumeNumber)) + if (volumes.Count >= 1 && volumes[0].MinNumber.IsNot(Scanner.Parser.LooseLeafVolumeNumber)) { - var looseLeafChapters = volumes.Where(v => v.MinNumber.Is(Parser.LooseLeafVolumeNumber)) + var looseLeafChapters = volumes.Where(v => v.MinNumber.Is(Scanner.Parser.LooseLeafVolumeNumber)) .SelectMany(c => c.Chapters.Where(c2 => !c2.IsSpecial)) .OrderBy(c => c.SortOrder) .ToList(); @@ -68,7 +67,7 @@ public static class SeriesExtensions } var chpts = volumes - .First(v => v.MinNumber.Is(Parser.LooseLeafVolumeNumber)) + .First(v => v.MinNumber.Is(Scanner.Parser.LooseLeafVolumeNumber)) .Chapters .Where(c => !c.IsSpecial) .OrderBy(c => c.MinNumber, ChapterSortComparerDefaultLast.Default) diff --git a/Kavita.Services/Extensions/VolumeExtensions.cs b/Kavita.Services/Extensions/VolumeExtensions.cs new file mode 100644 index 000000000..f508e871b --- /dev/null +++ b/Kavita.Services/Extensions/VolumeExtensions.cs @@ -0,0 +1,30 @@ +using Kavita.Common.Extensions; +using Kavita.Models.DTOs; + +namespace Kavita.Services.Extensions; + +public static class VolumeExtensions +{ + + extension(VolumeDto volumeDto) + { + /// + /// Is this a loose leaf volume + /// + /// + public bool IsLooseLeaf() + { + return volumeDto.MinNumber.Is(Scanner.Parser.LooseLeafVolumeNumber); + } + + /// + /// Does this volume hold only specials + /// + /// + public bool IsSpecial() + { + return volumeDto.MinNumber.Is(Scanner.Parser.SpecialVolumeNumber); + } + } + +} diff --git a/API/Extensions/VolumeListExtensions.cs b/Kavita.Services/Extensions/VolumeListExtensions.cs similarity index 77% rename from API/Extensions/VolumeListExtensions.cs rename to Kavita.Services/Extensions/VolumeListExtensions.cs index 2fa0446b4..218b5a3a0 100644 --- a/API/Extensions/VolumeListExtensions.cs +++ b/Kavita.Services/Extensions/VolumeListExtensions.cs @@ -1,14 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; -using API.Comparators; -using API.DTOs; -using API.Entities; -using API.Entities.Enums; -using API.Services.Tasks.Scanner.Parser; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Comparators; -namespace API.Extensions; -#nullable enable +namespace Kavita.Services.Extensions; public static class VolumeListExtensions { @@ -44,7 +43,7 @@ public static class VolumeListExtensions /// public static bool HasAnyNonLooseLeafVolumes(this IEnumerable volumes) { - return volumes.Any(v => v.MinNumber.IsNot(Parser.DefaultChapterNumber)); + return volumes.Any(v => v.MinNumber.IsNot(Scanner.Parser.DefaultChapterNumber)); } /// @@ -55,7 +54,7 @@ public static class VolumeListExtensions public static Volume? FirstNonLooseLeafOrDefault(this IEnumerable volumes) { return volumes.OrderBy(x => x.MinNumber, ChapterSortComparerDefaultLast.Default) - .FirstOrDefault(v => v.MinNumber.IsNot(Parser.DefaultChapterNumber)); + .FirstOrDefault(v => v.MinNumber.IsNot(Scanner.Parser.DefaultChapterNumber)); } /// @@ -65,7 +64,7 @@ public static class VolumeListExtensions /// public static Volume? GetLooseLeafVolumeOrDefault(this IEnumerable volumes) { - return volumes.FirstOrDefault(v => v.MinNumber.Is(Parser.DefaultChapterNumber)); + return volumes.FirstOrDefault(v => v.MinNumber.Is(Scanner.Parser.DefaultChapterNumber)); } /// @@ -75,16 +74,16 @@ public static class VolumeListExtensions /// public static Volume? GetSpecialVolumeOrDefault(this IEnumerable volumes) { - return volumes.FirstOrDefault(v => v.MinNumber.Is(Parser.SpecialVolumeNumber)); + return volumes.FirstOrDefault(v => v.MinNumber.Is(Scanner.Parser.SpecialVolumeNumber)); } public static IEnumerable WhereNotLooseLeaf(this IEnumerable volumes) { - return volumes.Where(v => v.MinNumber.Is(Parser.DefaultChapterNumber)); + return volumes.Where(v => v.MinNumber.Is(Scanner.Parser.DefaultChapterNumber)); } public static IEnumerable WhereLooseLeaf(this IEnumerable volumes) { - return volumes.Where(v => v.MinNumber.Is(Parser.DefaultChapterNumber)); + return volumes.Where(v => v.MinNumber.Is(Scanner.Parser.DefaultChapterNumber)); } } diff --git a/API/Extensions/ZipArchiveExtensions.cs b/Kavita.Services/Extensions/ZipArchiveExtensions.cs similarity index 70% rename from API/Extensions/ZipArchiveExtensions.cs rename to Kavita.Services/Extensions/ZipArchiveExtensions.cs index 8ed338e57..68763c4f6 100644 --- a/API/Extensions/ZipArchiveExtensions.cs +++ b/Kavita.Services/Extensions/ZipArchiveExtensions.cs @@ -1,14 +1,13 @@ -using System.IO; +using System.IO; using System.IO.Compression; using System.Linq; -namespace API.Extensions; -#nullable enable +namespace Kavita.Services.Extensions; public static class ZipArchiveExtensions { /// - /// Checks if archive has one or more files. Excludes directory entries. + /// Checks if the archive has one or more files. Excludes directory entries. /// /// /// diff --git a/API/Services/FileService.cs b/Kavita.Services/FileService.cs similarity index 87% rename from API/Services/FileService.cs rename to Kavita.Services/FileService.cs index 19a2952e1..8f19484fd 100644 --- a/API/Services/FileService.cs +++ b/Kavita.Services/FileService.cs @@ -3,17 +3,10 @@ using System.IO; using System.IO.Abstractions; using System.Security.Cryptography; using System.Text; -using API.Extensions; +using Kavita.API.Services; +using Kavita.Common.Extensions; -namespace API.Services; - -public interface IFileService -{ - IFileSystem GetFileSystem(); - bool HasFileBeenModifiedSince(string filePath, DateTime time); - bool Exists(string filePath); - bool ValidateSha(string filepath, string sha); -} +namespace Kavita.Services; public class FileService : IFileService { diff --git a/API/Services/FontService.cs b/Kavita.Services/FontService.cs similarity index 61% rename from API/Services/FontService.cs rename to Kavita.Services/FontService.cs index 7b8c9f230..489bd2727 100644 --- a/API/Services/FontService.cs +++ b/Kavita.Services/FontService.cs @@ -2,18 +2,21 @@ using System; using System.IO; using System.Linq; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.Entities; -using API.Entities.Enums.Font; -using API.Extensions; -using API.Services.Tasks.Scanner.Parser; using Flurl.Http; +using Kavita.API.Database; +using Kavita.API.Services; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums.Font; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; -namespace API.Services.Tasks; +namespace Kavita.Services; // Although we don't use all the fields, just including them all for completeness internal class GoogleFontsMetadata @@ -79,54 +82,33 @@ internal class GoogleFontsData public required int nanos { get; init; } } -public interface IFontService +public class FontService(IDirectoryService directoryService, IUnitOfWork unitOfWork, ILogger logger) + : IFontService { - Task CreateFontFromFileAsync(string path); - Task Delete(int fontId); - Task CreateFontFromUrl(string url); - Task IsFontInUse(int fontId); -} - -public class FontService: IFontService -{ - - public static readonly string DefaultFont = "Default"; - - private readonly IDirectoryService _directoryService; - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - private const string SupportedFontUrlPrefix = "https://fonts.google.com/"; private const string DownloadFontUrlPrefix = "https://fonts.google.com/download/list?family="; private const string GoogleFontsInvalidJsonPrefix = ")]}'"; - public FontService(IDirectoryService directoryService, IUnitOfWork unitOfWork, ILogger logger) + public async Task CreateFontFromFileAsync(string path, CancellationToken ct = default) { - _directoryService = directoryService; - _unitOfWork = unitOfWork; - _logger = logger; - } - - public async Task CreateFontFromFileAsync(string path) - { - if (!_directoryService.FileSystem.File.Exists(path)) + if (!directoryService.FileSystem.File.Exists(path)) { - _logger.LogInformation("Unable to create font from manual upload as font not in temp"); + logger.LogInformation("Unable to create font from manual upload as font not in temp"); throw new KavitaException("errors.font-manual-upload"); } - var fileName = _directoryService.FileSystem.FileInfo.New(path).Name; - var nakedFileName = _directoryService.FileSystem.Path.GetFileNameWithoutExtension(fileName); + var fileName = directoryService.FileSystem.FileInfo.New(path).Name; + var nakedFileName = directoryService.FileSystem.Path.GetFileNameWithoutExtension(fileName); var fontName = Parser.PrettifyFileName(nakedFileName); var normalizedName = Parser.Normalize(nakedFileName); - if (await _unitOfWork.EpubFontRepository.GetFontDtoByNameAsync(fontName) != null) + if (await unitOfWork.EpubFontRepository.GetFontDtoByNameAsync(fontName, ct) != null) { throw new KavitaException("errors.font-already-in-use"); } - _directoryService.CopyFileToDirectory(path, _directoryService.EpubFontDirectory); - var finalLocation = _directoryService.FileSystem.Path.Join(_directoryService.EpubFontDirectory, fileName); + directoryService.CopyFileToDirectory(path, directoryService.EpubFontDirectory); + var finalLocation = directoryService.FileSystem.Path.Join(directoryService.EpubFontDirectory, fileName); var font = new EpubFont() { @@ -135,10 +117,10 @@ public class FontService: IFontService FileName = Path.GetFileName(finalLocation), Provider = FontProvider.User }; - _unitOfWork.EpubFontRepository.Add(font); - await _unitOfWork.CommitAsync(); + unitOfWork.EpubFontRepository.Add(font); + await unitOfWork.CommitAsync(ct); - // TODO: Send update to UI + // default: Send update to UI return font; } @@ -146,15 +128,16 @@ public class FontService: IFontService /// This does not check if in use, use /// /// - public async Task Delete(int fontId) + /// + public async Task Delete(int fontId, CancellationToken ct = default) { - var font = await _unitOfWork.EpubFontRepository.GetFontAsync(fontId); + var font = await unitOfWork.EpubFontRepository.GetFontAsync(fontId, ct); if (font == null) return; await RemoveFont(font); } - public async Task CreateFontFromUrl(string url) + public async Task CreateFontFromUrl(string url, CancellationToken ct = default) { if (!url.StartsWith(SupportedFontUrlPrefix)) { @@ -163,69 +146,70 @@ public class FontService: IFontService // Extract Font name from url var fontFamily = url.Split(SupportedFontUrlPrefix)[1].Split("?")[0].Split("/").Last(); - _logger.LogInformation("Preparing to download {FontName} font", fontFamily.Sanitize()); + logger.LogInformation("Preparing to download {FontName} font", fontFamily.Sanitize()); var metaData = await GetGoogleFontsMetadataAsync(fontFamily); if (metaData == null) { - _logger.LogError("Unable to find metadata for {FontName}", fontFamily.Sanitize()); + logger.LogError("Unable to find metadata for {FontName}", fontFamily.Sanitize()); throw new KavitaException("errors.font-not-found"); } var googleFontRef = metaData.VariableFont(); if (googleFontRef == null) { - _logger.LogError("Unable to find variable font for {FontName} with metadata {MetaData}", fontFamily.Sanitize(), metaData); + logger.LogError("Unable to find variable font for {FontName} with metadata {MetaData}", fontFamily.Sanitize(), metaData); throw new KavitaException("errors.font-not-found"); } var fontExt = Path.GetExtension(googleFontRef.filename); var fileName = $"{fontFamily}{fontExt}"; - _logger.LogDebug("Downloading font {FontFamily} to {FileName} from {Url}", fontFamily.Sanitize(), fileName, googleFontRef.url); - var path = await googleFontRef.url.DownloadFileAsync(_directoryService.TempDirectory, fileName); + logger.LogDebug("Downloading font {FontFamily} to {FileName} from {Url}", fontFamily.Sanitize(), fileName.Sanitize(), googleFontRef.url); + var path = await googleFontRef.url.DownloadFileAsync(directoryService.TempDirectory, fileName, cancellationToken: ct); - return await CreateFontFromFileAsync(path); + return await CreateFontFromFileAsync(path, ct); } /// /// Returns if the given font is in use by any other user. System provided fonts will always return true. /// /// + /// /// - public async Task IsFontInUse(int fontId) + public async Task IsFontInUse(int fontId, CancellationToken ct = default) { - var font = await _unitOfWork.EpubFontRepository.GetFontAsync(fontId); + var font = await unitOfWork.EpubFontRepository.GetFontAsync(fontId, ct); if (font == null || font.Provider == FontProvider.System) return true; - return await _unitOfWork.EpubFontRepository.IsFontInUseAsync(fontId); + return await unitOfWork.EpubFontRepository.IsFontInUseAsync(fontId, ct); } - public async Task RemoveFont(EpubFont font) + private async Task RemoveFont(EpubFont font) { if (font.Provider == FontProvider.System) return; - var prefs = await _unitOfWork.UserRepository.GetAllPreferencesByFontAsync(font.Name); + var prefs = await unitOfWork.UserRepository.GetAllPreferencesByFontAsync(font.Name); foreach (var pref in prefs) { - pref.BookReaderFontFamily = DefaultFont; - _unitOfWork.UserRepository.Update(pref); + pref.BookReaderFontFamily = Defaults.DefaultFont; + unitOfWork.UserRepository.Update(pref); } try { // Copy the font file to temp for nightly removal (to give user time to reclaim if made a mistake) var existingLocation = - _directoryService.FileSystem.Path.Join(_directoryService.EpubFontDirectory, font.FileName); + directoryService.FileSystem.Path.Join(directoryService.EpubFontDirectory, font.FileName); var newLocation = - _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, font.FileName); - _directoryService.CopyFileToDirectory(existingLocation, newLocation); - _directoryService.DeleteFiles([existingLocation]); + directoryService.FileSystem.Path.Join(directoryService.TempDirectory, font.FileName); + directoryService.CopyFileToDirectory(existingLocation, newLocation); + directoryService.DeleteFiles([existingLocation]); } catch (Exception) { /* Swallow */ } - _unitOfWork.EpubFontRepository.Remove(font); - await _unitOfWork.CommitAsync(); + unitOfWork.EpubFontRepository.Remove(font); + await unitOfWork.CommitAsync(); } private async Task GetGoogleFontsMetadataAsync(string fontName) @@ -243,7 +227,7 @@ public class FontService: IFontService .GetStringAsync(); } catch (Exception ex) { - _logger.LogError(ex, "Unable to get metadata for {FontName} from {Url}", fontName.Sanitize(), url); + logger.LogError(ex, "Unable to get metadata for {FontName} from {Url}", fontName.Sanitize(), url.Sanitize()); return null; } diff --git a/API/Helpers/AnnotationHelper.cs b/Kavita.Services/Helpers/AnnotationHelper.cs similarity index 99% rename from API/Helpers/AnnotationHelper.cs rename to Kavita.Services/Helpers/AnnotationHelper.cs index 3b2fb4277..caddc9938 100644 --- a/API/Helpers/AnnotationHelper.cs +++ b/Kavita.Services/Helpers/AnnotationHelper.cs @@ -1,12 +1,11 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; -using API.DTOs.Reader; using HtmlAgilityPack; +using Kavita.Models.DTOs.Reader; -namespace API.Helpers; -#nullable enable +namespace Kavita.Services.Helpers; public static partial class AnnotationHelper { diff --git a/API/Helpers/BookChapterItemHelper.cs b/Kavita.Services/Helpers/BookChapterItemHelper.cs similarity index 96% rename from API/Helpers/BookChapterItemHelper.cs rename to Kavita.Services/Helpers/BookChapterItemHelper.cs index 05ac09e85..1da6f5367 100644 --- a/API/Helpers/BookChapterItemHelper.cs +++ b/Kavita.Services/Helpers/BookChapterItemHelper.cs @@ -1,9 +1,8 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using API.DTOs.Reader; +using Kavita.Models.DTOs.Reader; -namespace API.Helpers; -#nullable enable +namespace Kavita.Services.Helpers; public static class BookChapterItemHelper { diff --git a/API/Helpers/BookSortTitlePrefixHelper.cs b/Kavita.Services/Helpers/BookSortTitlePrefixHelper.cs similarity index 98% rename from API/Helpers/BookSortTitlePrefixHelper.cs rename to Kavita.Services/Helpers/BookSortTitlePrefixHelper.cs index c92df5d65..41a5f7774 100644 --- a/API/Helpers/BookSortTitlePrefixHelper.cs +++ b/Kavita.Services/Helpers/BookSortTitlePrefixHelper.cs @@ -1,8 +1,8 @@ -using System; +using System; using System.Collections.Generic; using System.Runtime.CompilerServices; -namespace API.Helpers; +namespace Kavita.Services.Helpers; /// /// Responsible for parsing book titles "The man on the street" and removing the prefix -> "man on the street". diff --git a/API/Helpers/CacheHelper.cs b/Kavita.Services/Helpers/CacheHelper.cs similarity index 84% rename from API/Helpers/CacheHelper.cs rename to Kavita.Services/Helpers/CacheHelper.cs index ede5caaef..79b340219 100644 --- a/API/Helpers/CacheHelper.cs +++ b/Kavita.Services/Helpers/CacheHelper.cs @@ -1,23 +1,10 @@ -using System; -using API.Entities; -using API.Entities.Interfaces; -using API.Services; +using System; +using Kavita.API.Services; +using Kavita.API.Services.Helpers; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Interfaces; -namespace API.Helpers; -#nullable enable - -public interface ICacheHelper -{ - bool ShouldUpdateCoverImage(string coverPath, MangaFile? firstFile, DateTime chapterCreated, - bool forceUpdate = false, - bool isCoverLocked = false); - - bool CoverImageExists(string path); - - bool IsFileUnmodifiedSinceCreationOrLastScan(IEntityDate chapter, bool forceUpdate, MangaFile? firstFile); - bool HasFileChangedSinceLastScan(DateTime lastScan, bool forceUpdate, MangaFile? firstFile); - -} +namespace Kavita.Services.Helpers; public class CacheHelper : ICacheHelper { diff --git a/API/Helpers/GenreHelper.cs b/Kavita.Services/Helpers/GenreHelper.cs similarity index 96% rename from API/Helpers/GenreHelper.cs rename to Kavita.Services/Helpers/GenreHelper.cs index 8580178d9..3d8654a3c 100644 --- a/API/Helpers/GenreHelper.cs +++ b/Kavita.Services/Helpers/GenreHelper.cs @@ -2,15 +2,14 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.DTOs.Metadata; -using API.Entities; -using API.Extensions; -using API.Helpers.Builders; +using Kavita.API.Database; +using Kavita.Common.Extensions; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.Entities; using Microsoft.EntityFrameworkCore; -namespace API.Helpers; -#nullable enable +namespace Kavita.Services.Helpers; public static class GenreHelper diff --git a/API/Helpers/KoreaderHelper.cs b/Kavita.Services/Helpers/KoreaderHelper.cs similarity index 98% rename from API/Helpers/KoreaderHelper.cs rename to Kavita.Services/Helpers/KoreaderHelper.cs index e5bbba5f3..a6d9ca343 100644 --- a/API/Helpers/KoreaderHelper.cs +++ b/Kavita.Services/Helpers/KoreaderHelper.cs @@ -1,12 +1,12 @@ -using API.DTOs.Progress; using System; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; +using Kavita.Models.DTOs.Progress; -namespace API.Helpers; +namespace Kavita.Services.Helpers; /// /// All things related to Koreader diff --git a/API/Helpers/PdfComicInfoExtractor.cs b/Kavita.Services/Helpers/PdfComicInfoExtractor.cs similarity index 79% rename from API/Helpers/PdfComicInfoExtractor.cs rename to Kavita.Services/Helpers/PdfComicInfoExtractor.cs index f0e0b1832..9663da4d2 100644 --- a/API/Helpers/PdfComicInfoExtractor.cs +++ b/Kavita.Services/Helpers/PdfComicInfoExtractor.cs @@ -5,31 +5,24 @@ * PDF 1.7 Specification a.k.a. PDF32000-1:2008 * https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf */ + using System; -using API.Data.Metadata; -using API.Entities.Enums; -using API.Services; -using API.Services.Tasks.Scanner.Parser; -using Microsoft.Extensions.Logging; -using Nager.ArticleNumber; using System.Collections.Generic; using System.Globalization; +using Kavita.API.Services; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Services.Extensions; +using Microsoft.Extensions.Logging; +using Nager.ArticleNumber; -namespace API.Helpers; -#nullable enable - -public interface IPdfComicInfoExtractor -{ - ComicInfo? GetComicInfo(string filePath); -} +namespace Kavita.Services.Helpers; /// /// Translate PDF metadata (See PdfMetadataExtractor.cs) into ComicInfo structure. /// -public class PdfComicInfoExtractor : IPdfComicInfoExtractor +public class PdfComicInfoExtractor(ILogger logger, IMediaErrorService mediaErrorService) { - private readonly ILogger _logger; - private readonly IMediaErrorService _mediaErrorService; private readonly string[] _pdfDateFormats = [ // PDF Spec 7.9.4 "D:yyyyMMddHHmmsszzz:", "D:yyyyMMddHHmmss+", "D:yyyyMMddHHmmss", "D:yyyyMMddHHmmzzz:", "D:yyyyMMddHHmm+", "D:yyyyMMddHHmm", @@ -37,12 +30,6 @@ public class PdfComicInfoExtractor : IPdfComicInfoExtractor "D:yyyyMMdd", "D:yyyyMM", "D:yyyy" ]; - public PdfComicInfoExtractor(ILogger logger, IMediaErrorService mediaErrorService) - { - _logger = logger; - _mediaErrorService = mediaErrorService; - } - private static float? GetFloatFromText(string? text) { if (string.IsNullOrEmpty(text)) return null; @@ -106,7 +93,7 @@ public class PdfComicInfoExtractor : IPdfComicInfoExtractor if (info.Isbn != string.Empty && !ArticleNumberHelper.IsValidIsbn10(info.Isbn) && !ArticleNumberHelper.IsValidIsbn13(info.Isbn)) { - _logger.LogDebug("[BookService] {File} has an invalid ISBN number", filePath); + logger.LogDebug("[BookService] {File} has an invalid ISBN number", filePath); info.Isbn = string.Empty; } @@ -116,12 +103,12 @@ public class PdfComicInfoExtractor : IPdfComicInfoExtractor info.Volume = MaybeGetMetadata(metadata, "Volume") ?? string.Empty; // If this is a single book and not a collection, set publication status to Completed - if (string.IsNullOrEmpty(info.Volume) && Parser.IsLooseLeafVolume(Parser.ParseVolume(filePath, LibraryType.Manga))) + if (string.IsNullOrEmpty(info.Volume) && Scanner.Parser.IsLooseLeafVolume(Scanner.Parser.ParseVolume(filePath, LibraryType.Manga))) { info.Count = 1; } - ComicInfo.CleanComicInfo(info); + info.CleanComicInfo(); return info; } @@ -130,14 +117,14 @@ public class PdfComicInfoExtractor : IPdfComicInfoExtractor { try { - using var extractor = new PdfMetadataExtractor(_logger, filePath); + using var extractor = new PdfMetadataExtractor(logger, filePath); return GetComicInfoFromMetadata(extractor.GetMetadata(), filePath); } catch (Exception ex) { - _logger.LogWarning(ex, "[GetComicInfo] There was an exception parsing PDF metadata for {File}", filePath); - _mediaErrorService.ReportMediaIssue(filePath, MediaErrorProducer.BookService, + logger.LogWarning(ex, "[GetComicInfo] There was an exception parsing PDF metadata for {File}", filePath); + mediaErrorService.ReportMediaIssue(filePath, MediaErrorProducer.BookService, ex.Message == "Encryption not supported" ? "Encrypted PDFs are not supported" : "There was an exception parsing PDF metadata", ex); diff --git a/API/Helpers/PdfMetadataExtractor.cs b/Kavita.Services/Helpers/PdfMetadataExtractor.cs similarity index 99% rename from API/Helpers/PdfMetadataExtractor.cs rename to Kavita.Services/Helpers/PdfMetadataExtractor.cs index ef959896f..09f2420ea 100644 --- a/API/Helpers/PdfMetadataExtractor.cs +++ b/Kavita.Services/Helpers/PdfMetadataExtractor.cs @@ -1,14 +1,13 @@ using System; using System.Collections.Generic; +using System.IO; using System.IO.Compression; using System.Text; using System.Xml; -using System.IO; -using API.Services; +using Kavita.API.Services; using Microsoft.Extensions.Logging; -namespace API.Helpers; -#nullable enable +namespace Kavita.Services.Helpers; // Contributed by https://github.com/microtherion @@ -804,7 +803,7 @@ internal class PdfLexer(Stream stream) internal class PdfMetadataExtractor : IPdfMetadataExtractor { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly PdfLexer _lexer; private readonly FileStream _stream; private readonly Dictionary _objectOffsets = []; @@ -824,7 +823,7 @@ internal class PdfMetadataExtractor : IPdfMetadataExtractor public readonly long Count = count; } - public PdfMetadataExtractor(ILogger logger, string filename) + public PdfMetadataExtractor(ILogger logger, string filename) { _logger = logger; _stream = File.OpenRead(filename); diff --git a/API/Helpers/PersonHelper.cs b/Kavita.Services/Helpers/PersonHelper.cs similarity index 96% rename from API/Helpers/PersonHelper.cs rename to Kavita.Services/Helpers/PersonHelper.cs index a23050800..efa0ea169 100644 --- a/API/Helpers/PersonHelper.cs +++ b/Kavita.Services/Helpers/PersonHelper.cs @@ -1,18 +1,16 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.Data.Metadata; -using API.DTOs; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Entities.Person; -using API.Extensions; -using API.Helpers.Builders; +using Kavita.API.Database; +using Kavita.Common.Extensions; +using Kavita.Models.Builders; +using Kavita.Models.DTOs; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.Person; -namespace API.Helpers; -#nullable enable +namespace Kavita.Services.Helpers; // This isn't needed in the new person architecture public static class PersonHelper @@ -223,7 +221,7 @@ public static class PersonHelper } - public static async Task UpdateChapterPeopleAsync(Chapter chapter, IList people, PersonRole role, IUnitOfWork unitOfWork) + public static async Task UpdateChapterPeopleAsync(Chapter chapter, IList people, PersonRole role, IUnitOfWork unitOfWork) { var modification = false; @@ -303,6 +301,8 @@ public static class PersonHelper { await unitOfWork.CommitAsync(); } + + return modification; } diff --git a/Kavita.Services/Helpers/ReviewHelper.cs b/Kavita.Services/Helpers/ReviewHelper.cs new file mode 100644 index 000000000..6de9ea988 --- /dev/null +++ b/Kavita.Services/Helpers/ReviewHelper.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using HtmlAgilityPack; +using Kavita.Models.DTOs.SeriesDetail; + +namespace Kavita.Services.Helpers; + +public static class ReviewHelper +{ + private const int BodyTextLimit = 175; + public static IEnumerable 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.OrderByDescending(r => r.Score); + } + +} diff --git a/API/Helpers/SeriesHelper.cs b/Kavita.Services/Helpers/SeriesHelper.cs similarity index 89% rename from API/Helpers/SeriesHelper.cs rename to Kavita.Services/Helpers/SeriesHelper.cs index 231575b0e..f19440267 100644 --- a/API/Helpers/SeriesHelper.cs +++ b/Kavita.Services/Helpers/SeriesHelper.cs @@ -1,12 +1,11 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Services.Tasks.Scanner; +using Kavita.Common.Extensions; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Parser; -namespace API.Helpers; -#nullable enable +namespace Kavita.Services.Helpers; public static class SeriesHelper { @@ -21,7 +20,7 @@ public static class SeriesHelper return (series.NormalizedName.Equals(parsedInfoKey.NormalizedName) || (series.LocalizedName != null && series.LocalizedName.ToNormalized().Equals(parsedInfoKey.NormalizedName)) || (series.OriginalName != null && series.OriginalName.ToNormalized().Equals(parsedInfoKey.NormalizedName)) - ) + ) && (series.Format == parsedInfoKey.Format || series.Format == MangaFormat.Unknown); } diff --git a/API/Helpers/SmartFilterHelper.cs b/Kavita.Services/Helpers/SmartFilterHelper.cs similarity index 98% rename from API/Helpers/SmartFilterHelper.cs rename to Kavita.Services/Helpers/SmartFilterHelper.cs index 8f61fde21..697d56fd6 100644 --- a/API/Helpers/SmartFilterHelper.cs +++ b/Kavita.Services/Helpers/SmartFilterHelper.cs @@ -2,12 +2,10 @@ using System.Collections.Generic; using System.Linq; using System.Web; -using API.DTOs.Filtering; -using API.DTOs.Filtering.v2; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.Filtering.v2; -#nullable enable - -namespace API.Helpers; +namespace Kavita.Services.Helpers; public static class SmartFilterHelper { diff --git a/API/Helpers/TagHelper.cs b/Kavita.Services/Helpers/TagHelper.cs similarity index 85% rename from API/Helpers/TagHelper.cs rename to Kavita.Services/Helpers/TagHelper.cs index c00d6ee8f..22dfe2176 100644 --- a/API/Helpers/TagHelper.cs +++ b/Kavita.Services/Helpers/TagHelper.cs @@ -3,16 +3,14 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.DTOs.Metadata; -using API.Entities; -using API.Extensions; -using API.Helpers.Builders; -using API.Services.Tasks.Scanner.Parser; +using Kavita.API.Database; +using Kavita.Common.Extensions; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.Entities; using Microsoft.EntityFrameworkCore; -namespace API.Helpers; -#nullable enable +namespace Kavita.Services.Helpers; public static class TagHelper { @@ -87,24 +85,6 @@ public static class TagHelper } } - /// - /// Returns a list of strings separated by ',', distinct by normalized names, already trimmed and empty entries removed. - /// - /// - /// - public static IList GetTagValues(string comicInfoTagSeparatedByComma) - { - // TODO: Refactor this into an Extension - if (string.IsNullOrEmpty(comicInfoTagSeparatedByComma)) - { - return ImmutableList.Empty; - } - - return comicInfoTagSeparatedByComma.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) - .DistinctBy(Parser.Normalize) - .ToList(); - } - public static void UpdateTagList(ICollection? existingDbTags, Series series, IReadOnlyCollection newTags, Action handleAdd, Action onModified) { diff --git a/API/Services/HostedServices/ReadingSessionInitializer.cs b/Kavita.Services/HostedServices/ReadingSessionInitializer.cs similarity index 96% rename from API/Services/HostedServices/ReadingSessionInitializer.cs rename to Kavita.Services/HostedServices/ReadingSessionInitializer.cs index bd4a652f3..2992337c5 100644 --- a/API/Services/HostedServices/ReadingSessionInitializer.cs +++ b/Kavita.Services/HostedServices/ReadingSessionInitializer.cs @@ -2,13 +2,13 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using API.Data; +using Kavita.Database; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace API.Services.HostedServices; +namespace Kavita.Services.HostedServices; public class ReadingSessionInitializer : IHostedService { diff --git a/API/Services/HostedServices/StartupTasksHostedService.cs b/Kavita.Services/HostedServices/StartupTasksHostedService.cs similarity index 83% rename from API/Services/HostedServices/StartupTasksHostedService.cs rename to Kavita.Services/HostedServices/StartupTasksHostedService.cs index 44a60849f..ec9697c20 100644 --- a/API/Services/HostedServices/StartupTasksHostedService.cs +++ b/Kavita.Services/HostedServices/StartupTasksHostedService.cs @@ -1,14 +1,14 @@ using System; using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.Services.Tasks.Scanner; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Scanner; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -namespace API.Services.HostedServices; -#nullable enable +namespace Kavita.Services.HostedServices; public class StartupTasksHostedService(IServiceProvider serviceProvider) : IHostedService { @@ -17,7 +17,7 @@ public class StartupTasksHostedService(IServiceProvider serviceProvider) : IHost using var scope = serviceProvider.CreateScope(); var taskScheduler = scope.ServiceProvider.GetRequiredService(); - await taskScheduler.ScheduleTasks(); + await taskScheduler.ScheduleTasks(cancellationToken); taskScheduler.ScheduleUpdaterTasks(); @@ -25,7 +25,7 @@ public class StartupTasksHostedService(IServiceProvider serviceProvider) : IHost { // These methods will automatically check if stat collection is disabled to prevent sending any data regardless // of when setting was changed - await taskScheduler.ScheduleStatsTasks(); + await taskScheduler.ScheduleStatsTasks(cancellationToken); await taskScheduler.RunStatCollection(); } catch (Exception) @@ -36,7 +36,7 @@ public class StartupTasksHostedService(IServiceProvider serviceProvider) : IHost try { var unitOfWork = scope.ServiceProvider.GetRequiredService(); - if ((await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableFolderWatching) + if ((await unitOfWork.SettingsRepository.GetSettingsDtoAsync(cancellationToken)).EnableFolderWatching) { var libraryWatcher = scope.ServiceProvider.GetRequiredService(); // Push this off for a bit for people with massive libraries, as it can take up to 45 mins and blocks the thread diff --git a/API/Services/ImageService.cs b/Kavita.Services/ImageService.cs similarity index 82% rename from API/Services/ImageService.cs rename to Kavita.Services/ImageService.cs index 37f315384..b85567fe4 100644 --- a/API/Services/ImageService.cs +++ b/Kavita.Services/ImageService.cs @@ -3,11 +3,14 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; +using System.Threading; using System.Threading.Tasks; -using API.DTOs; -using API.Entities.Enums; -using API.Entities.Interfaces; -using API.Extensions; +using Kavita.API.Services; +using Kavita.Models.DTOs; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Extensions; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; using NetVips; using SixLabors.ImageSharp.PixelFormats; @@ -15,60 +18,12 @@ using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; using Image = NetVips.Image; -namespace API.Services; -#nullable enable +namespace Kavita.Services; -public interface IImageService -{ - void ExtractImages(string fileFilePath, string targetDirectory, int fileCount = 1); - string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size); - - /// - /// Creates a Thumbnail version of a base64 image - /// - /// base64 encoded image - /// - /// Convert and save as encoding format - /// Width of thumbnail - /// If null, will write to - /// File name with extension of the file. - string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = 320, string? targetDirectory = null); - /// - /// Writes out a thumbnail by stream input - /// - /// - /// - /// - /// - /// - string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); - /// - /// Writes out a thumbnail by file path input - /// - /// - /// - /// - /// - /// - string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); - - /// - /// Converts the passed image to encoding and outputs it in the same directory - /// - /// Full path to the image to convert - /// Where to output the file - /// Encoding Format - /// File of written encoded image - Task ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat); - Task IsImage(string filePath); - void UpdateColorScape(IHasCoverImage entity); -} - -public class ImageService : IImageService +public class ImageService(ILogger logger, IDirectoryService directoryService) + : IImageService { public const string Name = "ImageService"; - private readonly ILogger _logger; - private readonly IDirectoryService _directoryService; public const string ChapterCoverImageRegex = @"v\d+_c\d+"; public const string SeriesCoverImageRegex = @"series\d+"; @@ -94,24 +49,18 @@ public class ImageService : IImageService public const int LibraryThumbnailWidth = 32; - public ImageService(ILogger logger, IDirectoryService directoryService) - { - _logger = logger; - _directoryService = directoryService; - } - public void ExtractImages(string? fileFilePath, string targetDirectory, int fileCount = 1) { if (string.IsNullOrEmpty(fileFilePath)) return; - _directoryService.ExistOrCreate(targetDirectory); + directoryService.ExistOrCreate(targetDirectory); if (fileCount == 1) { - _directoryService.CopyFileToDirectory(fileFilePath, targetDirectory); + directoryService.CopyFileToDirectory(fileFilePath, targetDirectory); } else { - _directoryService.CopyDirectoryToDirectory(_directoryService.FileSystem.Path.GetDirectoryName(fileFilePath), targetDirectory, - Tasks.Scanner.Parser.Parser.ImageFileExtensions); + directoryService.CopyDirectoryToDirectory(directoryService.FileSystem.Path.GetDirectoryName(fileFilePath), targetDirectory, + Parser.ImageFileExtensions); } } @@ -200,12 +149,12 @@ public class ImageService : IImageService size: GetSizeForDimensions(sourceImage, width, height), crop: GetCropForDimensions(sourceImage, width, height)); var filename = fileName + encodeFormat.GetExtension(); - thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); + thumbnail.WriteToFile(directoryService.FileSystem.Path.Join(outputDirectory, filename)); return filename; } catch (Exception ex) { - _logger.LogWarning(ex, "[GetCoverImage] There was an error and prevented thumbnail generation on {ImageFile}. Defaulting to no cover image", path); + logger.LogWarning(ex, "[GetCoverImage] There was an error and prevented thumbnail generation on {ImageFile}. Defaulting to no cover image", path); } return string.Empty; @@ -234,16 +183,16 @@ public class ImageService : IImageService crop: scalingCrop); var filename = fileName + encodeFormat.GetExtension(); - _directoryService.ExistOrCreate(outputDirectory); + directoryService.ExistOrCreate(outputDirectory); try { - _directoryService.FileSystem.File.Delete(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); + directoryService.FileSystem.File.Delete(directoryService.FileSystem.Path.Join(outputDirectory, filename)); } catch (Exception) {/* Swallow exception */} try { - thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); + thumbnail.WriteToFile(directoryService.FileSystem.Path.Join(outputDirectory, filename)); return filename; } @@ -256,7 +205,7 @@ public class ImageService : IImageService using var thumbnail2 = Image.ThumbnailStream(stream, targetWidth, height: targetHeight, size: scalingSize, crop: scalingCrop); - thumbnail2.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); + thumbnail2.WriteToFile(directoryService.FileSystem.Path.Join(outputDirectory, filename)); return filename; } @@ -271,18 +220,19 @@ public class ImageService : IImageService size: GetSizeForDimensions(sourceImage, width, height), crop: GetCropForDimensions(sourceImage, width, height)); var filename = fileName + encodeFormat.GetExtension(); - _directoryService.ExistOrCreate(outputDirectory); + directoryService.ExistOrCreate(outputDirectory); try { - _directoryService.FileSystem.File.Delete(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); + directoryService.FileSystem.File.Delete(directoryService.FileSystem.Path.Join(outputDirectory, filename)); } catch (Exception) {/* Swallow exception */} - thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); + thumbnail.WriteToFile(directoryService.FileSystem.Path.Join(outputDirectory, filename)); return filename; } - public Task ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat) + public Task ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat, + CancellationToken ct = default) { - var file = _directoryService.FileSystem.FileInfo.New(filePath); + var file = directoryService.FileSystem.FileInfo.New(filePath); var fileName = file.Name.Replace(file.Extension, string.Empty); var outputFile = Path.Join(outputPath, fileName + encodeFormat.GetExtension()); @@ -291,16 +241,11 @@ public class ImageService : IImageService return Task.FromResult(outputFile); } - /// - /// Performs I/O to determine if the file is a valid Image - /// - /// - /// - public async Task IsImage(string filePath) + public async Task IsImage(string filePath, CancellationToken ct = default) { try { - var info = await SixLabors.ImageSharp.Image.IdentifyAsync(filePath); + var info = await SixLabors.ImageSharp.Image.IdentifyAsync(filePath, ct); if (info == null) return false; return true; @@ -583,17 +528,17 @@ public class ImageService : IImageService // TODO: This code has no concept of cropping nor Thumbnail Size try { - targetDirectory ??= _directoryService.CoverImageDirectory; + targetDirectory ??= directoryService.CoverImageDirectory; using var thumbnail = Image.ThumbnailBuffer(Convert.FromBase64String(encodedImage), thumbnailWidth); fileName += encodeFormat.GetExtension(); - thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(targetDirectory, fileName)); + thumbnail.WriteToFile(directoryService.FileSystem.Path.Join(targetDirectory, fileName)); return fileName; } catch (Exception e) { - _logger.LogError(e, "Error creating thumbnail from url"); + logger.LogError(e, "Error creating thumbnail from url"); } return string.Empty; @@ -638,7 +583,7 @@ public class ImageService : IImageService /// public static string GetSeriesFormat(int seriesId) { - return $"series{seriesId}"; + return $"series{seriesId}"; // If this ever changes, also needs to update in SeriesRepository#GetAllWithCoversInDifferentEncoding } /// @@ -757,7 +702,7 @@ public class ImageService : IImageService public void UpdateColorScape(IHasCoverImage entity) { var colors = CalculateColorScape( - _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, entity.CoverImage)); + directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory, entity.CoverImage)); entity.PrimaryColor = colors.Primary; entity.SecondaryColor = colors.Secondary; } diff --git a/Kavita.Services/Kavita.Services.csproj b/Kavita.Services/Kavita.Services.csproj new file mode 100644 index 000000000..9affb3d48 --- /dev/null +++ b/Kavita.Services/Kavita.Services.csproj @@ -0,0 +1,70 @@ + + + + net10.0 + disable + enable + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/Kavita.Services/KoreaderService.cs b/Kavita.Services/KoreaderService.cs new file mode 100644 index 000000000..e8a0ff037 --- /dev/null +++ b/Kavita.Services/KoreaderService.cs @@ -0,0 +1,105 @@ +using System.Threading; +using System.Threading.Tasks; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Reading; +using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.Koreader; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Helpers; +using Microsoft.Extensions.Logging; + +namespace Kavita.Services; + +public class KoreaderService( + IReaderService readerService, + IUnitOfWork unitOfWork, + ILocalizationService localizationService, + ILogger logger) + : IKoreaderService +{ + /// + /// Given a Koreader hash, locate the underlying file and generate/update a progress event. + /// + /// + /// + /// + public async Task SaveProgress(KoreaderBookDto koreaderBookDto, int userId, CancellationToken ct = default) + { + logger.LogDebug("Saving Koreader progress for User ({UserId}): {KoreaderProgress}", userId, koreaderBookDto.progress.Sanitize()); + var file = await unitOfWork.MangaFileRepository.GetByKoreaderHash(koreaderBookDto.document, ct); + if (file == null) throw new KavitaException(await localizationService.Translate(userId, "file-missing")); + + var userProgressDto = await unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(file.ChapterId, userId, ct); + if (userProgressDto == null) + { + var chapterDto = await unitOfWork.ChapterRepository.GetChapterDtoAsync(file.ChapterId, userId, ct); + if (chapterDto == null) throw new KavitaException(await localizationService.Translate(userId, "chapter-doesnt-exist")); + + var volumeDto = await unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapterDto.VolumeId, ct: ct); + if (volumeDto == null) throw new KavitaException(await localizationService.Translate(userId, "volume-doesnt-exist")); + + var seriesDto = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(volumeDto.SeriesId, userId); + if (seriesDto == null) throw new KavitaException(await localizationService.Translate(userId, "series-doesnt-exist")); + + userProgressDto = new ProgressDto() + { + PageNum = 0, // This is updated in KoreaderHelper.UpdateProgressDto + ChapterId = file.ChapterId, + VolumeId = chapterDto.VolumeId, + SeriesId = seriesDto.Id, + LibraryId = seriesDto.LibraryId + }; + } + + // Update the bookScrollId if possible + var reportedProgress = koreaderBookDto.progress; + KoreaderHelper.UpdateProgressDto(userProgressDto, koreaderBookDto.progress); + + logger.LogDebug("Converted KOReader progress from {ProgressEncoding} to Page {PageNum} with ScrollId: {ScrollId}", reportedProgress.Sanitize(), + userProgressDto.PageNum, userProgressDto.BookScrollId?.Sanitize() ?? string.Empty); + + // Normal saving from kavita will be //body/h2[1] + await readerService.SaveReadingProgress(userProgressDto, userId); + } + + /// + /// Returns a Koreader Dto representing the current book and the progress within + /// + /// + /// + /// + /// + public async Task GetProgress(string bookHash, int userId, CancellationToken ct = default) + { + var settingsDto = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct); + + var file = await unitOfWork.MangaFileRepository.GetByKoreaderHash(bookHash, ct); + if (file == null) throw new KavitaException(await localizationService.Translate(userId, "file-missing")); + + var progressDto = await unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(file.ChapterId, userId, ct); + + // Non-epubs use the pageNum as the progress. KOReader is 1-index based + var koreaderProgress = $"{progressDto?.PageNum + 1 ?? 0}"; + if (file.Format == MangaFormat.Epub) + { + koreaderProgress = KoreaderHelper.GetKoreaderPosition(progressDto); + } + + var response = new KoreaderBookDtoBuilder(bookHash) + .WithProgress(koreaderProgress) + .WithPercentage(progressDto?.PageNum, file.Pages) + .WithDeviceId(settingsDto.InstallId, userId) + .WithTimestamp(progressDto?.LastModifiedUtc) + .Build(); + + logger.LogDebug("Responding to KOReader with Page {PageNum}, Scroll Id: {ScrollId}, and Progress: {Progress}", + progressDto?.PageNum, response.progress.Sanitize(), response.percentage); + + + return response; + } +} diff --git a/API/Services/LocalizationService.cs b/Kavita.Services/LocalizationService.cs similarity index 97% rename from API/Services/LocalizationService.cs rename to Kavita.Services/LocalizationService.cs index 31c9a2d0c..4a50868cd 100644 --- a/API/Services/LocalizationService.cs +++ b/Kavita.Services/LocalizationService.cs @@ -3,20 +3,13 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading.Tasks; -using API.Data; -using API.DTOs; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.Models.DTOs; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Hosting; -namespace API.Services; -#nullable enable - -public interface ILocalizationService -{ - Task Get(string locale, string key, params object[] args); - Task Translate(int userId, string key, params object[] args); - IEnumerable GetLocales(); -} +namespace Kavita.Services; public class LocalizationService : ILocalizationService { diff --git a/Kavita.Services/MediaConversionService.cs b/Kavita.Services/MediaConversionService.cs new file mode 100644 index 000000000..7a0769fbe --- /dev/null +++ b/Kavita.Services/MediaConversionService.cs @@ -0,0 +1,327 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Extensions; +using Kavita.Services.Comparators; +using Kavita.Services.Extensions; +using Microsoft.Extensions.Logging; + +namespace Kavita.Services; + +public class MediaConversionService( + IUnitOfWork unitOfWork, + IImageService imageService, + IEventHub eventHub, + IDirectoryService directoryService, + ILogger logger) + : IMediaConversionService +{ + public const string Name = "MediaConversionService"; + public static readonly string[] ConversionMethods = ["ConvertAllBookmarkToEncoding", "ConvertAllCoversToEncoding", "ConvertAllManagedMediaToEncodingFormat"]; + + /// + /// Converts all Kavita managed media (bookmarks, covers, favicons, etc) to the saved target encoding. + /// Do not invoke anyway except via Hangfire. + /// + /// + /// This is a long-running job + /// + [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)] + public async Task ConvertAllManagedMediaToEncodingFormat(CancellationToken ct = default) + { + await ConvertAllBookmarkToEncoding(ct); + await ConvertAllCoversToEncoding(ct); + await CoverAllFaviconsToEncoding(ct); + + } + + /// + /// This is a long-running job that will convert all bookmarks into a format that is not PNG. Do not invoke anyway except via Hangfire. + /// + /// + [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)] + public async Task ConvertAllBookmarkToEncoding(CancellationToken ct = default) + { + var bookmarkDirectory = + (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory, ct)).Value; + var encodeFormat = + (await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct)).EncodeMediaAs; + + if (encodeFormat == EncodeFormat.PNG) + { + logger.LogError("Cannot convert media to PNG"); + return; + } + + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertBookmarksProgressEvent(0F, ProgressEventType.Started), ct: ct); + + var bookmarks = (await unitOfWork.UserRepository.GetAllBookmarksAsync(ct)) + .Where(b => !b.FileName.EndsWith(encodeFormat.GetExtension())).ToList(); + + var count = 1F; + foreach (var bookmark in bookmarks) + { + bookmark.FileName = await SaveAsEncodingFormat(bookmarkDirectory, bookmark.FileName, + BookmarkService.BookmarkStem(bookmark.AppUserId, bookmark.SeriesId, bookmark.ChapterId), encodeFormat); + + unitOfWork.UserRepository.Update(bookmark); + + await unitOfWork.CommitAsync(ct); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertBookmarksProgressEvent(count / bookmarks.Count, ProgressEventType.Updated), ct: ct); + + count++; + } + + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertBookmarksProgressEvent(1F, ProgressEventType.Ended), ct: ct); + + logger.LogInformation("[MediaConversionService] Converted bookmarks to {Format}", encodeFormat); + } + + /// + /// This is a long-running job that will convert all covers into WebP. Do not invoke anyway except via Hangfire. + /// + /// + [DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)] + public async Task ConvertAllCoversToEncoding(CancellationToken ct = default) + { + var coverDirectory = directoryService.CoverImageDirectory; + var encodeFormat = + (await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct)).EncodeMediaAs; + + if (encodeFormat == EncodeFormat.PNG) + { + logger.LogError("Cannot convert media to PNG"); + return; + } + + logger.LogInformation("[MediaConversionService] Starting conversion of all covers to {Format}", encodeFormat); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertCoverProgressEvent(0F, ProgressEventType.Started), ct: ct); + + var chapterCovers = await unitOfWork.ChapterRepository.GetAllChaptersWithCoversInDifferentEncoding(encodeFormat, ct); + var customSeriesCovers = await unitOfWork.SeriesRepository.GetAllWithCoversInDifferentEncoding(encodeFormat); + var seriesCovers = await unitOfWork.SeriesRepository.GetAllWithCoversInDifferentEncoding(encodeFormat, false); + var nonCustomOrConvertedVolumeCovers = await unitOfWork.VolumeRepository.GetAllWithCoversInDifferentEncoding(encodeFormat, ct); + + var readingListCovers = await unitOfWork.ReadingListRepository.GetAllWithCoversInDifferentEncoding(encodeFormat, ct); + var libraryCovers = await unitOfWork.LibraryRepository.GetAllWithCoversInDifferentEncoding(encodeFormat, ct); + var collectionCovers = await unitOfWork.CollectionTagRepository.GetAllWithCoversInDifferentEncoding(encodeFormat, ct); + + var totalCount = chapterCovers.Count + seriesCovers.Count + readingListCovers.Count + + libraryCovers.Count + collectionCovers.Count + nonCustomOrConvertedVolumeCovers.Count + customSeriesCovers.Count; + + var count = 1F; + logger.LogInformation("[MediaConversionService] Starting conversion of chapters"); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertCoverProgressEvent(0, ProgressEventType.Started), ct: ct); + logger.LogInformation("[MediaConversionService] Starting conversion of libraries"); + foreach (var library in libraryCovers) + { + if (string.IsNullOrEmpty(library.CoverImage)) continue; + + var newFile = await SaveAsEncodingFormat(coverDirectory, library.CoverImage, coverDirectory, encodeFormat); + library.CoverImage = Path.GetFileName(newFile); + + unitOfWork.LibraryRepository.Update(library); + + await unitOfWork.CommitAsync(ct); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated), ct: ct); + + count++; + } + + logger.LogInformation("[MediaConversionService] Starting conversion of reading lists"); + foreach (var readingList in readingListCovers) + { + if (string.IsNullOrEmpty(readingList.CoverImage)) continue; + + var newFile = await SaveAsEncodingFormat(coverDirectory, readingList.CoverImage, coverDirectory, encodeFormat); + readingList.CoverImage = Path.GetFileName(newFile); + + unitOfWork.ReadingListRepository.Update(readingList); + + await unitOfWork.CommitAsync(ct); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated), ct: ct); + + count++; + } + + logger.LogInformation("[MediaConversionService] Starting conversion of collections"); + foreach (var collection in collectionCovers) + { + if (string.IsNullOrEmpty(collection.CoverImage)) continue; + + var newFile = await SaveAsEncodingFormat(coverDirectory, collection.CoverImage, coverDirectory, encodeFormat); + collection.CoverImage = Path.GetFileName(newFile); + + unitOfWork.CollectionTagRepository.Update(collection); + + await unitOfWork.CommitAsync(ct); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated), ct: ct); + + count++; + } + + logger.LogInformation("[MediaConversionService] Starting conversion of chapters"); + foreach (var chapter in chapterCovers) + { + if (string.IsNullOrEmpty(chapter.CoverImage)) continue; + + var newFile = await SaveAsEncodingFormat(coverDirectory, chapter.CoverImage, coverDirectory, encodeFormat); + chapter.CoverImage = Path.GetFileName(newFile); + + unitOfWork.ChapterRepository.Update(chapter); + + await unitOfWork.CommitAsync(ct); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated), ct: ct); + + count++; + } + + // Now null out all series and volumes that aren't webp or custom + logger.LogInformation("[MediaConversionService] Starting conversion of volumes"); + foreach (var volume in nonCustomOrConvertedVolumeCovers) + { + if (string.IsNullOrEmpty(volume.CoverImage)) continue; + volume.CoverImage = volume.Chapters.MinBy(x => x.MinNumber, ChapterSortComparerDefaultFirst.Default)?.CoverImage; + unitOfWork.VolumeRepository.Update(volume); + await unitOfWork.CommitAsync(ct); + } + + logger.LogInformation("[MediaConversionService] Starting conversion of series"); + foreach (var series in customSeriesCovers) + { + if (string.IsNullOrEmpty(series.CoverImage)) continue; + + var newFile = await SaveAsEncodingFormat(coverDirectory, series.CoverImage, coverDirectory, encodeFormat); + series.CoverImage = string.IsNullOrEmpty(newFile) ? + series.CoverImage.Replace(Path.GetExtension(series.CoverImage), encodeFormat.GetExtension()) : Path.GetFileName(newFile); + + unitOfWork.SeriesRepository.Update(series); + await unitOfWork.CommitAsync(ct); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated), ct: ct); + count++; + } + + foreach (var series in seriesCovers) + { + if (string.IsNullOrEmpty(series.CoverImage)) continue; + series.CoverImage = series.GetCoverImage(); + if (series.CoverImage == null) + { + logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null: {SeriesId}", series.Id); + } + unitOfWork.SeriesRepository.Update(series); + await unitOfWork.CommitAsync(ct); + } + + // Get all volumes and remap their covers + + // Get all series and remap their covers + + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertCoverProgressEvent(1F, ProgressEventType.Ended), ct: ct); + + logger.LogInformation("[MediaConversionService] Converted covers to {Format}", encodeFormat); + } + + private async Task CoverAllFaviconsToEncoding(CancellationToken ct = default) + { + var encodeFormat = + (await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct)).EncodeMediaAs; + + if (encodeFormat == EncodeFormat.PNG) + { + logger.LogError("Cannot convert media to PNG"); + return; + } + + logger.LogInformation("[MediaConversionService] Starting conversion of favicons to {Format}", encodeFormat); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertBookmarksProgressEvent(0F, ProgressEventType.Started), ct: ct); + var pngFavicons = directoryService.GetFiles(directoryService.FaviconDirectory) + .Where(b => !b.EndsWith(encodeFormat.GetExtension())). + ToList(); + + var count = 1F; + foreach (var file in pngFavicons) + { + await SaveAsEncodingFormat(directoryService.FaviconDirectory, directoryService.FileSystem.FileInfo.New(file).Name, directoryService.FaviconDirectory, + encodeFormat); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertBookmarksProgressEvent(count / pngFavicons.Count, ProgressEventType.Updated), ct: ct); + count++; + } + + + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.ConvertBookmarksProgressEvent(1F, ProgressEventType.Ended), ct: ct); + + logger.LogInformation("[MediaConversionService] Converted favicons to {Format}", encodeFormat); + } + + + /// + /// Converts an image file, deletes original and returns the new path back + /// + /// Full Path to where files are stored + /// The file to convert + /// Full path to where files should be stored or any stem + /// Encoding Format + /// + public async Task SaveAsEncodingFormat(string imageDirectory, string filename, string targetFolder, EncodeFormat encodeFormat) + { + // This must be Public as it's used in via Hangfire as a background task + var fullSourcePath = directoryService.FileSystem.Path.Join(imageDirectory, filename); + var fullTargetDirectory = fullSourcePath.Replace(new FileInfo(filename).Name, string.Empty); + + var newFilename = string.Empty; + logger.LogDebug("Converting {Source} image into {Encoding} at {Target}", fullSourcePath, encodeFormat, fullTargetDirectory); + + if (!File.Exists(fullSourcePath)) + { + logger.LogError("Requested to convert {File} but it doesn't exist", fullSourcePath); + return newFilename; + } + + try + { + // Convert target file to format then delete original target file + try + { + var targetFile = await imageService.ConvertToEncodingFormat(fullSourcePath, fullTargetDirectory, encodeFormat); + var targetName = new FileInfo(targetFile).Name; + newFilename = Path.Join(targetFolder, targetName); + directoryService.DeleteFiles([fullSourcePath]); + } + catch (Exception ex) + { + logger.LogError(ex, "Could not convert image {FilePath} to {Format}", filename, encodeFormat); + newFilename = filename; + } + } + catch (Exception ex) + { + logger.LogError(ex, "Could not convert image to {Format}", encodeFormat); + } + + return newFilename; + } + +} diff --git a/Kavita.Services/MediaErrorService.cs b/Kavita.Services/MediaErrorService.cs new file mode 100644 index 000000000..2ec0ad235 --- /dev/null +++ b/Kavita.Services/MediaErrorService.cs @@ -0,0 +1,52 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.Models.Builders; +using Kavita.Models.Entities.Enums; + +namespace Kavita.Services; + + + +public class MediaErrorService(IUnitOfWork unitOfWork) : IMediaErrorService +{ + public void ReportMediaIssue(string filename, MediaErrorProducer producer, string errorMessage, Exception ex) + { + // TODO: Localize all these messages + // To avoid overhead on commits, do async. We don't need to wait. + BackgroundJob.Enqueue(() => ReportMediaIssueAsync(filename, producer, errorMessage, ex.Message, CancellationToken.None)); + } + + public void ReportMediaIssue(string filename, MediaErrorProducer producer, string errorMessage, string details) + { + // To avoid overhead on commits, do async. We don't need to wait. + BackgroundJob.Enqueue(() => ReportMediaIssueAsync(filename, producer, errorMessage, details, CancellationToken.None)); + } + + public async Task ReportMediaIssueAsync(string filename, MediaErrorProducer producer, string errorMessage, Exception ex, CancellationToken ct = default) + { + await ReportMediaIssueAsync(filename, producer, errorMessage, ex.Message, ct); + } + + public async Task ReportMediaIssueAsync(string filename, MediaErrorProducer producer, string errorMessage, + string details, CancellationToken ct = default) + { + var error = new MediaErrorBuilder(filename) + .WithComment(errorMessage) + .WithDetails(details) + .Build(); + + if (await unitOfWork.MediaErrorRepository.ExistsAsync(error, ct)) + { + return; + } + + + unitOfWork.MediaErrorRepository.Attach(error); + await unitOfWork.CommitAsync(ct); + } + +} diff --git a/API/Services/Tasks/Metadata/CoverDbService.cs b/Kavita.Services/Metadata/CoverDbService.cs similarity index 92% rename from API/Services/Tasks/Metadata/CoverDbService.cs rename to Kavita.Services/Metadata/CoverDbService.cs index af4310116..caed47c08 100644 --- a/API/Services/Tasks/Metadata/CoverDbService.cs +++ b/Kavita.Services/Metadata/CoverDbService.cs @@ -2,41 +2,32 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Person; -using API.Extensions; -using API.SignalR; using EasyCaching.Core; using Flurl; using Flurl.Http; using HtmlAgilityPack; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Metadata; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Person; +using Kavita.Models.Entities.User; +using Kavita.Models.Extensions; +using Kavita.Services.Repositories; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using NetVips; - -namespace API.Services.Tasks.Metadata; -#nullable enable - -public interface ICoverDbService -{ - Task DownloadFaviconAsync(string url, EncodeFormat encodeFormat); - Task DownloadPublisherImageAsync(string publisherName, EncodeFormat encodeFormat); - Task DownloadPersonImageAsync(Person person, EncodeFormat encodeFormat); - Task DownloadPersonImageAsync(Person person, EncodeFormat encodeFormat, string url); - Task SetPersonCoverByUrl(Person person, string url, bool fromBase64 = true, bool checkNoImagePlaceholder = false, bool chooseBetterImage = true); - Task SetSeriesCoverByUrl(Series series, string url, bool fromBase64 = true, bool chooseBetterImage = false); - Task SetChapterCoverByUrl(Chapter chapter, string url, bool fromBase64 = true, bool chooseBetterImage = false); - Task SetUserCoverByUrl(int userId, string url, bool fromBase64 = true, bool chooseBetterImage = false); - Task SetUserCoverByUrl(AppUser user, string url, bool fromBase64 = true, bool chooseBetterImage = false); -} - +namespace Kavita.Services.Metadata; public class CoverDbService : ICoverDbService { @@ -88,6 +79,7 @@ public class CoverDbService : ICoverDbService /// /// The full URL of the website to extract the favicon from. /// The desired image encoding format for saving the favicon (e.g., WebP, PNG). + /// /// /// A string representing the filename of the downloaded favicon image, saved to the configured favicon directory. /// @@ -99,7 +91,7 @@ public class CoverDbService : ICoverDbService /// It then attempts to parse HTML for `link` tags pointing to `.png` favicons and /// falls back to an internal fallback method if needed. Valid results are saved to disk. /// - public async Task DownloadFaviconAsync(string url, EncodeFormat encodeFormat) + public async Task DownloadFaviconAsync(string url, EncodeFormat encodeFormat, CancellationToken ct = default) { // Parse the URL to get the domain (including subdomain) var uri = new Uri(url); @@ -108,7 +100,7 @@ public class CoverDbService : ICoverDbService var provider = _cacheFactory.GetCachingProvider(EasyCacheProfiles.Favicon); - var res = await provider.GetAsync(baseUrl); + var res = await provider.GetAsync(baseUrl, ct); if (res.HasValue) { var sanitizedBaseUrl = baseUrl.Sanitize(); @@ -116,7 +108,7 @@ public class CoverDbService : ICoverDbService throw new KavitaException($"Kavita has already tried to fetch from {sanitizedBaseUrl} and failed. Skipping duplicate check"); } - await provider.SetAsync(baseUrl, string.Empty, _cacheTime); + await provider.SetAsync(baseUrl, string.Empty, _cacheTime, ct); if (FaviconUrlMapper.TryGetValue(baseUrl, out var value)) { url = value; @@ -126,7 +118,7 @@ public class CoverDbService : ICoverDbService try { - var htmlContent = url.GetStringAsync().Result; + var htmlContent = url.GetStringAsync(cancellationToken: ct).Result; var htmlDocument = new HtmlDocument(); htmlDocument.LoadHtml(htmlContent); @@ -170,7 +162,7 @@ public class CoverDbService : ICoverDbService // Download the favicon.ico file using Flurl var faviconStream = await finalUrl .AllowHttpStatus("2xx,304") - .GetStreamAsync(); + .GetStreamAsync(cancellationToken: ct); // Create the destination file path using var image = Image.PngloadStream(faviconStream); @@ -187,21 +179,22 @@ public class CoverDbService : ICoverDbService } } - public async Task DownloadPublisherImageAsync(string publisherName, EncodeFormat encodeFormat) + public async Task DownloadPublisherImageAsync(string publisherName, EncodeFormat encodeFormat, + CancellationToken ct = default) { try { // Sanitize user input publisherName = publisherName.Replace(Environment.NewLine, string.Empty).Replace("\r", string.Empty).Replace("\n", string.Empty); var provider = _cacheFactory.GetCachingProvider(EasyCacheProfiles.Publisher); - var res = await provider.GetAsync(publisherName); + var res = await provider.GetAsync(publisherName, ct); if (res.HasValue) { _logger.LogInformation("Kavita has already tried to fetch Publisher: {PublisherName} and failed. Skipping duplicate check", publisherName); throw new KavitaException($"Kavita has already tried to fetch Publisher: {publisherName} and failed. Skipping duplicate check"); } - await provider.SetAsync(publisherName, string.Empty, _cacheTime); + await provider.SetAsync(publisherName, string.Empty, _cacheTime, ct); var publisherLink = await FallbackToKavitaReaderPublisher(publisherName); if (string.IsNullOrEmpty(publisherLink)) { @@ -229,8 +222,10 @@ public class CoverDbService : ICoverDbService /// /// /// + /// /// Person image (in correct directory) or null if not found/error - public async Task DownloadPersonImageAsync(Person person, EncodeFormat encodeFormat) + public async Task DownloadPersonImageAsync(Person person, EncodeFormat encodeFormat, + CancellationToken ct = default) { try { @@ -239,7 +234,7 @@ public class CoverDbService : ICoverDbService { throw new KavitaException($"Could not grab person image for {person.Name}"); } - return await DownloadPersonImageAsync(person, encodeFormat, personImageLink); + return await DownloadPersonImageAsync(person, encodeFormat, personImageLink, ct); } catch (Exception ex) { _logger.LogError(ex, "Error downloading image for {PersonName}", person.Name); @@ -254,10 +249,12 @@ public class CoverDbService : ICoverDbService /// /// /// + /// /// /// /// - public async Task DownloadPersonImageAsync(Person person, EncodeFormat encodeFormat, string url) + public async Task DownloadPersonImageAsync(Person person, EncodeFormat encodeFormat, string url, + CancellationToken ct = default) { try { @@ -283,7 +280,7 @@ public class CoverDbService : ICoverDbService private async Task DownloadImageFromUrl(string filenameWithoutExtension, EncodeFormat encodeFormat, string url, string? targetDirectory = null) { - // TODO: I need to unit test this to ensure it works when overwriting, etc + // default: I need to unit test this to ensure it works when overwriting, etc // Target Directory defaults to CoverImageDirectory, but can be temp for when comparison between images is used targetDirectory ??= _directoryService.CoverImageDirectory; @@ -475,7 +472,9 @@ public class CoverDbService : ICoverDbService /// /// Will check against all known null image placeholders to avoid writing it /// If we check cross-reference the current cover for the better option - public async Task SetPersonCoverByUrl(Person person, string url, bool fromBase64 = true, bool checkNoImagePlaceholder = false, bool chooseBetterImage = true) + /// + public async Task SetPersonCoverByUrl(Person person, string url, bool fromBase64 = true, + bool checkNoImagePlaceholder = false, bool chooseBetterImage = true, CancellationToken ct = default) { if (!string.IsNullOrEmpty(url)) { @@ -555,9 +554,9 @@ public class CoverDbService : ICoverDbService if (_unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, - MessageFactory.CoverUpdateEvent(person.Id, MessageFactoryEntityTypes.Person), false); + MessageFactory.CoverUpdateEvent(person.Id, MessageFactoryEntityTypes.Person), false, ct); } } @@ -568,7 +567,9 @@ public class CoverDbService : ICoverDbService /// /// /// If images are similar, will choose the higher quality image - public async Task SetSeriesCoverByUrl(Series series, string url, bool fromBase64 = true, bool chooseBetterImage = false) + /// + public async Task SetSeriesCoverByUrl(Series series, string url, bool fromBase64 = true, + bool chooseBetterImage = false, CancellationToken ct = default) { if (!string.IsNullOrEmpty(url)) { @@ -638,14 +639,15 @@ public class CoverDbService : ICoverDbService if (_unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, - MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series), false); + MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series), false, ct); } } - // TODO: Refactor this to IHasCoverImage instead of a hard entity type - public async Task SetChapterCoverByUrl(Chapter chapter, string url, bool fromBase64 = true, bool chooseBetterImage = false) + // default: Refactor this to IHasCoverImage instead of a hard entity type + public async Task SetChapterCoverByUrl(Chapter chapter, string url, bool fromBase64 = true, + bool chooseBetterImage = false, CancellationToken ct = default) { if (!string.IsNullOrEmpty(url)) { @@ -711,24 +713,25 @@ public class CoverDbService : ICoverDbService if (_unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); await _eventHub.SendMessageAsync( MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter), - false - ); + false, ct); } } - public async Task SetUserCoverByUrl(int userId, string url, bool fromBase64 = true, bool chooseBetterImage = false) + public async Task SetUserCoverByUrl(int userId, string url, bool fromBase64 = true, bool chooseBetterImage = false, + CancellationToken ct = default) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences, ct); if (user == null) return; - await SetUserCoverByUrl(user, url, fromBase64, chooseBetterImage); + await SetUserCoverByUrl(user, url, fromBase64, chooseBetterImage, ct); } - public async Task SetUserCoverByUrl(AppUser user, string url, bool fromBase64 = true, bool chooseBetterImage = false) + public async Task SetUserCoverByUrl(AppUser user, string url, bool fromBase64 = true, + bool chooseBetterImage = false, CancellationToken ct = default) { if (!string.IsNullOrEmpty(url)) { @@ -763,9 +766,9 @@ public class CoverDbService : ICoverDbService if (_unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, - MessageFactory.CoverUpdateEvent(user.Id, MessageFactoryEntityTypes.User), false); + MessageFactory.CoverUpdateEvent(user.Id, MessageFactoryEntityTypes.User), false, ct); } } diff --git a/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs b/Kavita.Services/Metadata/WordCountAnalyzerService.cs similarity index 61% rename from API/Services/Tasks/Metadata/WordCountAnalyzerService.cs rename to Kavita.Services/Metadata/WordCountAnalyzerService.cs index 1a6bb5ab3..6470002d2 100644 --- a/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs +++ b/Kavita.Services/Metadata/WordCountAnalyzerService.cs @@ -1,86 +1,72 @@ using System; using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.Entities; -using API.Entities.Enums; -using API.Helpers; -using API.Services.Reading; -using API.SignalR; using Hangfire; using HtmlAgilityPack; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Helpers; +using Kavita.API.Services.Metadata; +using Kavita.API.Services.SignalR; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Reading; using Microsoft.Extensions.Logging; using VersOne.Epub; -namespace API.Services.Tasks.Metadata; -#nullable enable - -public interface IWordCountAnalyzerService -{ - [DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)] - [AutomaticRetry(Attempts = 2, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - Task ScanLibrary(int libraryId, bool forceUpdate = false); - Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = true); -} +namespace Kavita.Services.Metadata; /// /// This service is a metadata task that generates information around time to read /// -public class WordCountAnalyzerService : IWordCountAnalyzerService +public class WordCountAnalyzerService( + ILogger logger, + IUnitOfWork unitOfWork, + IEventHub eventHub, + ICacheHelper cacheHelper, + IMediaErrorService mediaErrorService) + : IWordCountAnalyzerService { - private readonly ILogger _logger; - private readonly IUnitOfWork _unitOfWork; - private readonly IEventHub _eventHub; - private readonly ICacheHelper _cacheHelper; - private readonly IMediaErrorService _mediaErrorService; - public const int AverageCharactersPerWord = 5; - public WordCountAnalyzerService(ILogger logger, IUnitOfWork unitOfWork, IEventHub eventHub, - ICacheHelper cacheHelper, IMediaErrorService mediaErrorService) - { - _logger = logger; - _unitOfWork = unitOfWork; - _eventHub = eventHub; - _cacheHelper = cacheHelper; - _mediaErrorService = mediaErrorService; - } - [DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)] [AutomaticRetry(Attempts = 2, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - public async Task ScanLibrary(int libraryId, bool forceUpdate = false) + public async Task ScanLibrary(int libraryId, bool forceUpdate = false, CancellationToken ct = default) { var sw = Stopwatch.StartNew(); - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); + var library = await unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, ct: ct); if (library == null) return; - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.WordCountAnalyzerProgressEvent(libraryId, 0F, ProgressEventType.Started, string.Empty)); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.WordCountAnalyzerProgressEvent(libraryId, 0F, ProgressEventType.Started, string.Empty), ct: ct); - var chunkInfo = await _unitOfWork.SeriesRepository.GetChunkInfo(library.Id); + var chunkInfo = await unitOfWork.SeriesRepository.GetChunkInfo(library.Id, ct); var stopwatch = Stopwatch.StartNew(); - _logger.LogInformation("[MetadataService] Refreshing Library {LibraryName}. Total Items: {TotalSize}. Total Chunks: {TotalChunks} with {ChunkSize} size", library.Name, chunkInfo.TotalSize, chunkInfo.TotalChunks, chunkInfo.ChunkSize); + logger.LogInformation("[MetadataService] Refreshing Library {LibraryName}. Total Items: {TotalSize}. Total Chunks: {TotalChunks} with {ChunkSize} size", library.Name, chunkInfo.TotalSize, chunkInfo.TotalChunks, chunkInfo.ChunkSize); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.WordCountAnalyzerProgressEvent(library.Id, 0F, ProgressEventType.Started, $"Starting {library.Name}")); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.WordCountAnalyzerProgressEvent(library.Id, 0F, ProgressEventType.Started, $"Starting {library.Name}"), ct: ct); for (var chunk = 1; chunk <= chunkInfo.TotalChunks; chunk++) { if (chunkInfo.TotalChunks == 0) continue; stopwatch.Restart(); - _logger.LogInformation("[MetadataService] Processing chunk {ChunkNumber} / {TotalChunks} with size {ChunkSize}. Series ({SeriesStart} - {SeriesEnd}", + logger.LogInformation("[MetadataService] Processing chunk {ChunkNumber} / {TotalChunks} with size {ChunkSize}. Series ({SeriesStart} - {SeriesEnd}", chunk, chunkInfo.TotalChunks, chunkInfo.ChunkSize, chunk * chunkInfo.ChunkSize, (chunk + 1) * chunkInfo.ChunkSize); - var nonLibrarySeries = await _unitOfWork.SeriesRepository.GetFullSeriesForLibraryIdAsync(library.Id, + var nonLibrarySeries = await unitOfWork.SeriesRepository.GetFullSeriesForLibraryIdAsync(library.Id, new UserParams() { PageNumber = chunk, PageSize = chunkInfo.ChunkSize - }); - _logger.LogDebug("[MetadataService] Fetched {SeriesCount} series for refresh", nonLibrarySeries.Count); + }, ct); + logger.LogDebug("[MetadataService] Fetched {SeriesCount} series for refresh", nonLibrarySeries.Count); var seriesIndex = 0; foreach (var series in nonLibrarySeries) @@ -88,8 +74,8 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService var index = chunk * seriesIndex; var progress = Math.Max(0F, Math.Min(1F, index * 1F / chunkInfo.TotalSize)); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.WordCountAnalyzerProgressEvent(library.Id, progress, ProgressEventType.Updated, series.Name)); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.WordCountAnalyzerProgressEvent(library.Id, progress, ProgressEventType.Updated, series.Name), ct: ct); try { @@ -97,53 +83,53 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService } catch (Exception ex) { - _logger.LogError(ex, "[MetadataService] There was an exception during metadata refresh for {SeriesName}", series.Name); + logger.LogError(ex, "[MetadataService] There was an exception during metadata refresh for {SeriesName}", series.Name); } seriesIndex++; } - if (_unitOfWork.HasChanges()) + if (unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); } - _logger.LogInformation( + logger.LogInformation( "[MetadataService] Processed {SeriesStart} - {SeriesEnd} out of {TotalSeries} series in {ElapsedScanTime} milliseconds for {LibraryName}", chunk * chunkInfo.ChunkSize, (chunk * chunkInfo.ChunkSize) + nonLibrarySeries.Count, chunkInfo.TotalSize, stopwatch.ElapsedMilliseconds, library.Name); } - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.WordCountAnalyzerProgressEvent(library.Id, 1F, ProgressEventType.Ended, $"Complete")); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.WordCountAnalyzerProgressEvent(library.Id, 1F, ProgressEventType.Ended, $"Complete"), ct: ct); - _logger.LogInformation("[WordCountAnalyzerService] Updated metadata for {LibraryName} in {ElapsedMilliseconds} milliseconds", library.Name, sw.ElapsedMilliseconds); + logger.LogInformation("[WordCountAnalyzerService] Updated metadata for {LibraryName} in {ElapsedMilliseconds} milliseconds", library.Name, sw.ElapsedMilliseconds); } - public async Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = true) + public async Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = true, CancellationToken ct = default) { var sw = Stopwatch.StartNew(); - var series = await _unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(seriesId); + var series = await unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(seriesId, ct); if (series == null) { - _logger.LogError("[WordCountAnalyzerService] Series {SeriesId} was not found on Library {LibraryId}", seriesId, libraryId); + logger.LogError("[WordCountAnalyzerService] Series {SeriesId} was not found on Library {LibraryId}", seriesId, libraryId); return; } - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.WordCountAnalyzerProgressEvent(libraryId, 0F, ProgressEventType.Started, series.Name)); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.WordCountAnalyzerProgressEvent(libraryId, 0F, ProgressEventType.Started, series.Name), ct: ct); await ProcessSeries(series, forceUpdate); - if (_unitOfWork.HasChanges()) + if (unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); } - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.WordCountAnalyzerProgressEvent(libraryId, 1F, ProgressEventType.Ended, series.Name)); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.WordCountAnalyzerProgressEvent(libraryId, 1F, ProgressEventType.Ended, series.Name), ct: ct); - _logger.LogInformation("[WordCountAnalyzerService] Updated metadata for {SeriesName} in {ElapsedMilliseconds} milliseconds", series.Name, sw.ElapsedMilliseconds); + logger.LogInformation("[WordCountAnalyzerService] Updated metadata for {SeriesName} in {ElapsedMilliseconds} milliseconds", series.Name, sw.ElapsedMilliseconds); } @@ -159,7 +145,7 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService { // This compares if it's changed since a file scan only var firstFile = chapter.Files.FirstOrDefault(); - if (firstFile == null || !_cacheHelper.HasFileChangedSinceLastScan(firstFile.LastFileAnalysis, + if (firstFile == null || !cacheHelper.HasFileChangedSinceLastScan(firstFile.LastFileAnalysis, forceUpdate, firstFile)) { @@ -178,7 +164,7 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService var pageCounter = 1; try { - // TODO: Replace with BookService method, we will loose progress but these tasks are usually fast + // default: Replace with BookService method, we will loose progress but these tasks are usually fast using var book = await EpubReader.OpenBookAsync(filePath, BookService.LenientBookReaderOptions); var totalPages = book.Content.Html.Local; @@ -187,7 +173,7 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService var progress = Math.Max(0F, Math.Min(1F, (fileCounter * pageCounter) * 1F / (chapter.Files.Count * totalPages.Count))); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.WordCountAnalyzerProgressEvent(series.LibraryId, progress, ProgressEventType.Updated, useFileName ? filePath : series.Name)); sum += await GetWordCountFromHtml(bookPage, filePath); @@ -198,8 +184,8 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService } catch (Exception ex) { - _logger.LogError(ex, "There was an error reading an epub file for word count, series skipped"); - await _eventHub.SendMessageAsync(MessageFactory.Error, + logger.LogError(ex, "There was an error reading an epub file for word count, series skipped"); + await eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent("There was an issue counting words on an epub", $"{series.Name} - {file.FilePath}")); return; @@ -222,14 +208,14 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService { UpdateFileAnalysis(file); } - _unitOfWork.ChapterRepository.Update(chapter); + unitOfWork.ChapterRepository.Update(chapter); } var volumeEst = ReaderService.GetTimeEstimate(volume.WordCount, volume.Pages, isEpub); volume.MinHoursToRead = volumeEst.MinHours; volume.MaxHoursToRead = volumeEst.MaxHours; volume.AvgHoursToRead = volumeEst.AvgHours; - _unitOfWork.VolumeRepository.Update(volume); + unitOfWork.VolumeRepository.Update(volume); } @@ -238,13 +224,13 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService series.MinHoursToRead = seriesEstimate.MinHours; series.MaxHoursToRead = seriesEstimate.MaxHours; series.AvgHoursToRead = seriesEstimate.AvgHours; - _unitOfWork.SeriesRepository.Update(series); + unitOfWork.SeriesRepository.Update(series); } private void UpdateFileAnalysis(MangaFile file) { file.UpdateLastFileAnalysis(); - _unitOfWork.MangaFileRepository.Update(file); + unitOfWork.MangaFileRepository.Update(file); } private async Task GetWordCountFromHtml(EpubLocalTextContentFileRef bookFile, string filePath) @@ -260,8 +246,8 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService } catch (EpubContentException ex) { - _logger.LogError(ex, "Error when counting words in epub {EpubPath}", filePath); - await _mediaErrorService.ReportMediaIssueAsync(filePath, MediaErrorProducer.BookService, + logger.LogError(ex, "Error when counting words in epub {EpubPath}", filePath); + await mediaErrorService.ReportMediaIssueAsync(filePath, MediaErrorProducer.BookService, $"Invalid Epub Metadata, {bookFile.FilePath} does not exist", ex.Message); return 0; } diff --git a/API/Services/MetadataService.cs b/Kavita.Services/MetadataService.cs similarity index 58% rename from API/Services/MetadataService.cs rename to Kavita.Services/MetadataService.cs index f9b07d4e7..15e2acce6 100644 --- a/API/Services/MetadataService.cs +++ b/Kavita.Services/MetadataService.cs @@ -2,73 +2,42 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.Comparators; -using API.Data; -using API.DTOs.Settings; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Interfaces; -using API.Extensions; -using API.Helpers; -using API.SignalR; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Helpers; +using Kavita.API.Services.SignalR; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; +using Kavita.Services.Comparators; +using Kavita.Services.Extensions; using Microsoft.Extensions.Logging; -namespace API.Services; -#nullable enable - -public interface IMetadataService -{ - /// - /// Recalculates cover images for all entities in a library. - /// - /// - /// - [DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)] - [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - Task GenerateCoversForLibrary(int libraryId, bool forceUpdate = false, bool forceColorScape = false); - /// - /// Performs a forced refresh of cover images just for a series, and it's nested entities - /// - /// - /// - /// Overrides any cache logic and forces execution - - Task GenerateCoversForSeries(ServerSettingDto serverSetting, int libraryId, int seriesId, bool forceUpdate = true, bool forceColorScape = true); - Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceUpdate = false, bool forceColorScape = true); - Task RemoveAbandonedMetadataKeys(); -} +namespace Kavita.Services; /// /// Handles everything around Cover/ColorScape management /// -public class MetadataService : IMetadataService +public class MetadataService( + IUnitOfWork unitOfWork, + ILogger logger, + IEventHub eventHub, + ICacheHelper cacheHelper, + IReadingItemService readingItemService, + IDirectoryService directoryService, + IImageService imageService) + : IMetadataService { public const string Name = "MetadataService"; - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - private readonly IEventHub _eventHub; - private readonly ICacheHelper _cacheHelper; - private readonly IReadingItemService _readingItemService; - private readonly IDirectoryService _directoryService; - private readonly IImageService _imageService; private readonly IList _updateEvents = new List(); - public MetadataService(IUnitOfWork unitOfWork, ILogger logger, - IEventHub eventHub, ICacheHelper cacheHelper, - IReadingItemService readingItemService, IDirectoryService directoryService, - IImageService imageService) - { - _unitOfWork = unitOfWork; - _logger = logger; - _eventHub = eventHub; - _cacheHelper = cacheHelper; - _readingItemService = readingItemService; - _directoryService = directoryService; - _imageService = imageService; - } - /// /// Updates the metadata for a Chapter /// @@ -83,14 +52,14 @@ public class MetadataService : IMetadataService var firstFile = chapter.Files.MinBy(x => x.Chapter); if (firstFile == null) return false; - if (!_cacheHelper.ShouldUpdateCoverImage( - _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, chapter.CoverImage), + if (!cacheHelper.ShouldUpdateCoverImage( + directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory, chapter.CoverImage), firstFile, chapter.Created, forceUpdate, chapter.CoverImageLocked)) { if (NeedsColorSpace(chapter, forceColorScape)) { - _imageService.UpdateColorScape(chapter); - _unitOfWork.ChapterRepository.Update(chapter); + imageService.UpdateColorScape(chapter); + unitOfWork.ChapterRepository.Update(chapter); _updateEvents.Add(MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter)); } @@ -98,14 +67,14 @@ public class MetadataService : IMetadataService } - _logger.LogDebug("[MetadataService] Generating cover image for {File}", firstFile.FilePath); + logger.LogDebug("[MetadataService] Generating cover image for {File}", firstFile.FilePath); - chapter.CoverImage = _readingItemService.GetCoverImage(firstFile.FilePath, + chapter.CoverImage = readingItemService.GetCoverImage(firstFile.FilePath, ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId), firstFile.Format, encodeFormat, coverImageSize); - _imageService.UpdateColorScape(chapter); + imageService.UpdateColorScape(chapter); - _unitOfWork.ChapterRepository.Update(chapter); + unitOfWork.ChapterRepository.Update(chapter); _updateEvents.Add(MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter)); return true; @@ -114,7 +83,7 @@ public class MetadataService : IMetadataService private void UpdateChapterLastModified(Chapter chapter, bool forceUpdate) { var firstFile = chapter.Files.MinBy(x => x.Chapter); - if (firstFile == null || _cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, forceUpdate, firstFile)) return; + if (firstFile == null || cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, forceUpdate, firstFile)) return; firstFile.UpdateLastModified(); } @@ -141,14 +110,14 @@ public class MetadataService : IMetadataService // We need to check if Volume coverImage matches first chapters if forceUpdate is false if (volume == null) return false; - if (!_cacheHelper.ShouldUpdateCoverImage( - _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, volume.CoverImage), + if (!cacheHelper.ShouldUpdateCoverImage( + directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory, volume.CoverImage), null, volume.Created, forceUpdate)) { if (NeedsColorSpace(volume, forceColorScape)) { - _imageService.UpdateColorScape(volume); - _unitOfWork.VolumeRepository.Update(volume); + imageService.UpdateColorScape(volume); + unitOfWork.VolumeRepository.Update(volume); _updateEvents.Add(MessageFactory.CoverUpdateEvent(volume.Id, MessageFactoryEntityTypes.Volume)); } return false; @@ -168,7 +137,7 @@ public class MetadataService : IMetadataService volume.CoverImage = firstChapter.CoverImage; } - _imageService.UpdateColorScape(volume); + imageService.UpdateColorScape(volume); _updateEvents.Add(MessageFactory.CoverUpdateEvent(volume.Id, MessageFactoryEntityTypes.Volume)); @@ -184,14 +153,14 @@ public class MetadataService : IMetadataService { if (series == null) return; - if (!_cacheHelper.ShouldUpdateCoverImage( - _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, series.CoverImage), + if (!cacheHelper.ShouldUpdateCoverImage( + directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory, series.CoverImage), null, series.Created, forceUpdate, series.CoverImageLocked)) { // Check if we don't have a primary/seconary color if (NeedsColorSpace(series, forceColorScape)) { - _imageService.UpdateColorScape(series); + imageService.UpdateColorScape(series); _updateEvents.Add(MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series)); } @@ -202,10 +171,10 @@ public class MetadataService : IMetadataService series.CoverImage = series.GetCoverImage(); if (series.CoverImage == null) { - _logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null: {SeriesId}", series.Id); + logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null: {SeriesId}", series.Id); } - _imageService.UpdateColorScape(series); + imageService.UpdateColorScape(series); _updateEvents.Add(MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series)); } @@ -219,7 +188,7 @@ public class MetadataService : IMetadataService /// private async Task ProcessSeriesCoverGen(Series series, bool forceUpdate, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceColorScape = false) { - _logger.LogDebug("[MetadataService] Processing cover image generation for series: {SeriesName}", series.OriginalName); + logger.LogDebug("[MetadataService] Processing cover image generation for series: {SeriesName}", series.OriginalName); try { var totalVolumes = series.Volumes.Count; @@ -248,7 +217,7 @@ public class MetadataService : IMetadataService firstVolumeUpdated = true; } - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.CoverUpdateProgressEvent(series.LibraryId, volumeIndex / (float) totalVolumes, ProgressEventType.Started, series.Name)); volumeIndex++; @@ -258,7 +227,7 @@ public class MetadataService : IMetadataService } catch (Exception ex) { - _logger.LogError(ex, "[MetadataService] There was an exception during cover generation for {SeriesName} ", series.Name); + logger.LogError(ex, "[MetadataService] There was an exception during cover generation for {SeriesName} ", series.Name); } } @@ -270,25 +239,27 @@ public class MetadataService : IMetadataService /// /// Force updating cover image even if underlying file has not been modified or chapter already has a cover image /// Force updating colorscape + /// [DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - public async Task GenerateCoversForLibrary(int libraryId, bool forceUpdate = false, bool forceColorScape = false) + public async Task GenerateCoversForLibrary(int libraryId, bool forceUpdate = false, bool forceColorScape = false, + CancellationToken ct = default) { - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); + var library = await unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, ct: ct); if (library == null) return; - _logger.LogInformation("[MetadataService] Beginning cover generation refresh of {LibraryName}", library.Name); + logger.LogInformation("[MetadataService] Beginning cover generation refresh of {LibraryName}", library.Name); _updateEvents.Clear(); - var chunkInfo = await _unitOfWork.SeriesRepository.GetChunkInfo(library.Id); + var chunkInfo = await unitOfWork.SeriesRepository.GetChunkInfo(library.Id, ct); var stopwatch = Stopwatch.StartNew(); var totalTime = 0L; - _logger.LogInformation("[MetadataService] Refreshing Library {LibraryName} for cover generation. Total Items: {TotalSize}. Total Chunks: {TotalChunks} with {ChunkSize} size", library.Name, chunkInfo.TotalSize, chunkInfo.TotalChunks, chunkInfo.ChunkSize); + logger.LogInformation("[MetadataService] Refreshing Library {LibraryName} for cover generation. Total Items: {TotalSize}. Total Chunks: {TotalChunks} with {ChunkSize} size", library.Name, chunkInfo.TotalSize, chunkInfo.TotalChunks, chunkInfo.ChunkSize); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.CoverUpdateProgressEvent(library.Id, 0F, ProgressEventType.Started, $"Starting {library.Name}")); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.CoverUpdateProgressEvent(library.Id, 0F, ProgressEventType.Started, $"Starting {library.Name}"), ct: ct); - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct); var encodeFormat = settings.EncodeMediaAs; var coverImageSize = settings.CoverImageSize; @@ -298,16 +269,16 @@ public class MetadataService : IMetadataService totalTime += stopwatch.ElapsedMilliseconds; stopwatch.Restart(); - _logger.LogDebug("[MetadataService] Processing chunk {ChunkNumber} / {TotalChunks} with size {ChunkSize}. Series ({SeriesStart} - {SeriesEnd})", + logger.LogDebug("[MetadataService] Processing chunk {ChunkNumber} / {TotalChunks} with size {ChunkSize}. Series ({SeriesStart} - {SeriesEnd})", chunk, chunkInfo.TotalChunks, chunkInfo.ChunkSize, chunk * chunkInfo.ChunkSize, (chunk + 1) * chunkInfo.ChunkSize); - var nonLibrarySeries = await _unitOfWork.SeriesRepository.GetFullSeriesForLibraryIdAsync(library.Id, + var nonLibrarySeries = await unitOfWork.SeriesRepository.GetFullSeriesForLibraryIdAsync(library.Id, new UserParams() { PageNumber = chunk, PageSize = chunkInfo.ChunkSize - }); - _logger.LogDebug("[MetadataService] Fetched {SeriesCount} series for refresh", nonLibrarySeries.Count); + }, ct); + logger.LogDebug("[MetadataService] Fetched {SeriesCount} series for refresh", nonLibrarySeries.Count); var seriesIndex = 0; foreach (var series in nonLibrarySeries) @@ -315,8 +286,8 @@ public class MetadataService : IMetadataService var index = chunk * seriesIndex; var progress = Math.Max(0F, Math.Min(1F, index * 1F / chunkInfo.TotalSize)); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.CoverUpdateProgressEvent(library.Id, progress, ProgressEventType.Updated, series.Name)); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.CoverUpdateProgressEvent(library.Id, progress, ProgressEventType.Updated, series.Name), ct: ct); try { @@ -324,57 +295,60 @@ public class MetadataService : IMetadataService } catch (Exception ex) { - _logger.LogError(ex, "[MetadataService] There was an exception during cover generation refresh for {SeriesName}", series.Name); + logger.LogError(ex, "[MetadataService] There was an exception during cover generation refresh for {SeriesName}", series.Name); } seriesIndex++; } - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); await FlushEvents(); - _logger.LogInformation( + logger.LogInformation( "[MetadataService] Processed {SeriesStart} - {SeriesEnd} out of {TotalSeries} series in {ElapsedScanTime} milliseconds for {LibraryName}", chunk * chunkInfo.ChunkSize, (chunk * chunkInfo.ChunkSize) + nonLibrarySeries.Count, chunkInfo.TotalSize, stopwatch.ElapsedMilliseconds, library.Name); } - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.CoverUpdateProgressEvent(library.Id, 1F, ProgressEventType.Ended, $"Complete")); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.CoverUpdateProgressEvent(library.Id, 1F, ProgressEventType.Ended, $"Complete"), ct: ct); - _logger.LogInformation("[MetadataService] Updated covers for {SeriesNumber} series in library {LibraryName} in {ElapsedMilliseconds} milliseconds total", chunkInfo.TotalSize, library.Name, totalTime); + logger.LogInformation("[MetadataService] Updated covers for {SeriesNumber} series in library {LibraryName} in {ElapsedMilliseconds} milliseconds total", chunkInfo.TotalSize, library.Name, totalTime); } - public async Task RemoveAbandonedMetadataKeys() + public async Task RemoveAbandonedMetadataKeys(CancellationToken ct = default) { - await _unitOfWork.TagRepository.RemoveAllTagNoLongerAssociated(); - await _unitOfWork.PersonRepository.RemoveAllPeopleNoLongerAssociated(); - await _unitOfWork.GenreRepository.RemoveAllGenreNoLongerAssociated(); - await _unitOfWork.CollectionTagRepository.RemoveCollectionsWithoutSeries(); - await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters(); + await unitOfWork.TagRepository.RemoveAllTagNoLongerAssociated(ct); + await unitOfWork.PersonRepository.RemoveAllPeopleNoLongerAssociated(ct); + await unitOfWork.GenreRepository.RemoveAllGenreNoLongerAssociated(ct: ct); + await unitOfWork.CollectionTagRepository.RemoveCollectionsWithoutSeries(ct); + await unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters(ct); } /// /// Refreshes Metadata for a Series. Will always force updates. /// + /// /// /// /// Overrides any cache logic and forces execution /// Will ensure that the colorscape is regenerated - public async Task GenerateCoversForSeries(ServerSettingDto serverSetting, int libraryId, int seriesId, bool forceUpdate = true, bool forceColorScape = true) + /// + public async Task GenerateCoversForSeries(ServerSettingDto serverSetting, int libraryId, int seriesId, + bool forceUpdate = true, bool forceColorScape = true, CancellationToken ct = default) { - var series = await _unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(seriesId); + var series = await unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(seriesId, ct); if (series == null) { - _logger.LogError("[MetadataService] Series {SeriesId} was not found on Library {LibraryId}", seriesId, libraryId); + logger.LogError("[MetadataService] Series {SeriesId} was not found on Library {LibraryId}", seriesId, libraryId); return; } var encodeFormat = serverSetting.EncodeMediaAs; var coverImageSize = serverSetting.CoverImageSize; - await GenerateCoversForSeries(series, encodeFormat, coverImageSize, forceUpdate, forceColorScape); + await GenerateCoversForSeries(series, encodeFormat, coverImageSize, forceUpdate, forceColorScape, ct); } /// @@ -382,37 +356,40 @@ public class MetadataService : IMetadataService /// /// A full Series, with metadata, chapters, etc /// When saving the file, what encoding should be used + /// /// /// Forces just colorscape generation - public async Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceUpdate = false, bool forceColorScape = true) + /// + public async Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, CoverImageSize coverImageSize, + bool forceUpdate = false, bool forceColorScape = true, CancellationToken ct = default) { var sw = Stopwatch.StartNew(); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.CoverUpdateProgressEvent(series.LibraryId, 0F, ProgressEventType.Started, series.Name)); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.CoverUpdateProgressEvent(series.LibraryId, 0F, ProgressEventType.Started, series.Name), ct: ct); await ProcessSeriesCoverGen(series, forceUpdate, encodeFormat, coverImageSize, forceColorScape); - if (_unitOfWork.HasChanges()) + if (unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); - _logger.LogInformation("[MetadataService] Updated covers for {SeriesName} in {ElapsedMilliseconds} milliseconds", series.Name, sw.ElapsedMilliseconds); + await unitOfWork.CommitAsync(ct); + logger.LogInformation("[MetadataService] Updated covers for {SeriesName} in {ElapsedMilliseconds} milliseconds", series.Name, sw.ElapsedMilliseconds); } - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.CoverUpdateProgressEvent(series.LibraryId, 1F, ProgressEventType.Ended, series.Name)); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.CoverUpdateProgressEvent(series.LibraryId, 1F, ProgressEventType.Ended, series.Name), ct: ct); - await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series), false); + await eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series), false, ct); await FlushEvents(); } private async Task FlushEvents() { // Send all events out now that entities are saved - _logger.LogDebug("Dispatching {Count} update events", _updateEvents.Count); + logger.LogDebug("Dispatching {Count} update events", _updateEvents.Count); foreach (var updateEvent in _updateEvents) { - await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, updateEvent, false); + await eventHub.SendMessageAsync(MessageFactory.CoverUpdate, updateEvent, false); } _updateEvents.Clear(); } diff --git a/API/Services/OidcService.cs b/Kavita.Services/OidcService.cs similarity index 93% rename from API/Services/OidcService.cs rename to Kavita.Services/OidcService.cs index b9207b100..b2ae26735 100644 --- a/API/Services/OidcService.cs +++ b/Kavita.Services/OidcService.cs @@ -1,27 +1,29 @@ -#nullable enable -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Security.Claims; +using System.Threading; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Email; -using API.DTOs.Settings; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Progress; -using API.Extensions; -using API.Helpers.Builders; -using API.Services.Tasks.Metadata; using Hangfire; using Flurl.Http; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Metadata; using Kavita.Common; using Kavita.Common.EnvironmentInfo; +using Kavita.Common.Extensions; +using Kavita.Models.Builders; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.Email; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; +using Kavita.Services.Extensions; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http; @@ -33,34 +35,10 @@ using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; -namespace API.Services; - -public interface IOidcService -{ - /// - /// Returns the user authenticated with OpenID Connect - /// - /// - /// - /// - /// if any requirements aren't met - Task LoginOrCreate(HttpRequest request, ClaimsPrincipal principal); - /// - /// Refresh the token inside the cookie when it's close to expiring. And sync the user - /// - /// - /// - /// If the token is refreshed successfully, updates the last active time of the suer - Task RefreshCookieToken(CookieValidatePrincipalContext ctx); - /// - /// Remove from all users - /// - /// - Task ClearOidcIds(); -} +namespace Kavita.Services; /// -/// The ConfigurationManager will refresh the configuration periodically to ensure the data stays up to date +/// The ConfigurationManager will refresh the configuration periodically to ensure the data stays up to date. /// We can store the same one indefinitely as the authority does not change unless Kavita is restarted /// /// The ConfigurationManager has its own lock, it loads data thread safe @@ -84,9 +62,10 @@ public class OidcService(ILogger logger, UserManager userM private static readonly ConcurrentDictionary RefreshInProgress = new(); private static readonly ConcurrentDictionary LastFailedRefresh = new(); - public async Task LoginOrCreate(HttpRequest request, ClaimsPrincipal principal) + public async Task LoginOrCreate(HttpRequest request, ClaimsPrincipal principal, + CancellationToken ct = default) { - var settings = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; + var settings = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct)).OidcConfig; var oidcId = principal.FindFirstValue(ClaimTypes.NameIdentifier); if (string.IsNullOrEmpty(oidcId)) @@ -94,7 +73,7 @@ public class OidcService(ILogger logger, UserManager userM throw new KavitaException("errors.oidc.missing-external-id"); } - var user = await unitOfWork.UserRepository.GetByOidcId(oidcId, AppUserIncludes.UserPreferences | AppUserIncludes.SideNavStreams); + var user = await unitOfWork.UserRepository.GetByOidcId(oidcId, AppUserIncludes.UserPreferences | AppUserIncludes.SideNavStreams, ct); if (user != null) { await SyncUserSettings(request, settings, principal, user); @@ -114,7 +93,7 @@ public class OidcService(ILogger logger, UserManager userM } - user = await unitOfWork.UserRepository.GetUserByEmailAsync(email, AppUserIncludes.UserPreferences | AppUserIncludes.SideNavStreams); + user = await unitOfWork.UserRepository.GetUserByEmailAsync(email, AppUserIncludes.UserPreferences | AppUserIncludes.SideNavStreams, ct); if (user != null) { // Don't allow taking over accounts @@ -126,7 +105,7 @@ public class OidcService(ILogger logger, UserManager userM logger.LogDebug("User {UserName} has matched on email to {OidcId}", user.Id, oidcId); user.OidcId = oidcId; - await unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); await SyncUserSettings(request, settings, principal, user); @@ -136,11 +115,11 @@ public class OidcService(ILogger logger, UserManager userM return await CreateNewAccount(request, principal, settings, oidcId); } - public async Task RefreshCookieToken(CookieValidatePrincipalContext ctx) + public async Task RefreshCookieToken(CookieValidatePrincipalContext ctx, CancellationToken ct = default) { if (ctx.Principal == null) return null; - var user = await unitOfWork.UserRepository.GetUserByIdAsync(ctx.Principal.GetUserId()) ?? throw new UnauthorizedAccessException(); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(ctx.Principal.GetUserId(), ct: ct) ?? throw new UnauthorizedAccessException(); var key = ctx.Principal.GetUsername(); var refreshToken = ctx.Properties.GetTokenValue(RefreshToken); @@ -160,7 +139,7 @@ public class OidcService(ILogger logger, UserManager userM try { - var settings = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; + var settings = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct)).OidcConfig; var tokenResponse = await RefreshTokenAsync(settings, refreshToken); if (tokenResponse == null || !string.IsNullOrEmpty(tokenResponse.Error)) @@ -194,19 +173,19 @@ public class OidcService(ILogger logger, UserManager userM return user; } - public async Task ClearOidcIds() + public async Task ClearOidcIds(CancellationToken ct = default) { - var users = await unitOfWork.UserRepository.GetAllUsersAsync(); + var users = await unitOfWork.UserRepository.GetAllUsersAsync(ct: ct); foreach (var user in users) { user.OidcId = null; } - await unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); } /// - /// Tries to construct a new account from the OIDC Principal, may fail if required conditions aren't met + /// Tries to construct a new account from the OIDC Principal may fail if required conditions aren't met /// /// /// @@ -423,7 +402,7 @@ public class OidcService(ILogger logger, UserManager userM // Will just need to be documented on the wiki. if (!string.IsNullOrEmpty(picture) && string.IsNullOrEmpty(user.CoverImage)) { - // Run in background to not block http thread, pass id to Hangfire doesn't kill itself + // Run in the background to not block http thread, pass id to Hangfire doesn't kill itself BackgroundJob.Enqueue(() => coverDbService.SetUserCoverByUrl(user.Id, picture, false)); } } diff --git a/API/Services/OpdsService.cs b/Kavita.Services/OpdsService.cs similarity index 74% rename from API/Services/OpdsService.cs rename to Kavita.Services/OpdsService.cs index a24d71ace..97a95be25 100644 --- a/API/Services/OpdsService.cs +++ b/Kavita.Services/OpdsService.cs @@ -2,68 +2,41 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using System.Xml; using System.Xml.Serialization; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Filtering; -using API.DTOs.Filtering.v2; -using API.DTOs.OPDS; -using API.DTOs.OPDS.Requests; -using API.DTOs.Person; -using API.DTOs.ReadingLists; -using API.DTOs.Search; -using API.Entities; -using API.Entities.Enums; -using API.Exceptions; -using API.Helpers; -using API.Helpers.Formatting; -using API.Services.Reading; -using API.Services.Tasks.Scanner.Parser; +using Kavita.API.Database; +using Kavita.API.Errors; +using Kavita.API.Services; +using Kavita.API.Services.Reading; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.DTOs.OPDS; +using Kavita.Models.DTOs.OPDS.Requests; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.DTOs.Search; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Services.Helpers; -namespace API.Services; -#nullable enable +namespace Kavita.Services; -public interface IOpdsService +public class OpdsService( + IUnitOfWork unitOfWork, + ILocalizationService localizationService, + ISeriesService seriesService, + IDownloadService downloadService, + IDirectoryService directoryService, + IReaderService readerService, + IEntityNamingService namingService, + IReadingListService readingListService) + : IOpdsService { - Task GetCatalogue(OpdsCatalogueRequest request); - Task GetSmartFilters(OpdsPaginatedCatalogueRequest request); - Task GetLibraries(OpdsPaginatedCatalogueRequest request); - Task GetWantToRead(OpdsPaginatedCatalogueRequest request); - Task GetCollections(OpdsPaginatedCatalogueRequest request); - Task GetReadingLists(OpdsPaginatedCatalogueRequest request); - Task GetRecentlyAdded(OpdsPaginatedCatalogueRequest request); - Task GetRecentlyUpdated(OpdsPaginatedCatalogueRequest request); - Task GetOnDeck(OpdsPaginatedCatalogueRequest request); - - Task GetMoreInGenre(OpdsItemsFromEntityIdRequest request); - Task GetSeriesFromSmartFilter(OpdsItemsFromEntityIdRequest request); - Task GetSeriesFromCollection(OpdsItemsFromEntityIdRequest request); - Task GetSeriesFromLibrary(OpdsItemsFromEntityIdRequest request); - Task GetReadingListItems(OpdsItemsFromEntityIdRequest request); - Task GetSeriesDetail(OpdsItemsFromEntityIdRequest request); - Task GetItemsFromVolume(OpdsItemsFromCompoundEntityIdsRequest request); - Task GetItemsFromChapter(OpdsItemsFromCompoundEntityIdsRequest request); - - Task Search(OpdsSearchRequest request); - - string SerializeXml(Feed? feed); -} - -public class OpdsService : IOpdsService -{ - private readonly IUnitOfWork _unitOfWork; - private readonly ILocalizationService _localizationService; - private readonly ISeriesService _seriesService; - private readonly IDownloadService _downloadService; - private readonly IDirectoryService _directoryService; - private readonly IReaderService _readerService; - private readonly IEntityNamingService _namingService; - private readonly IReadingListService _readingListService; - - private readonly XmlSerializer _xmlSerializer; + private readonly XmlSerializer _xmlSerializer = new(typeof(Feed)); public const int PageSize = 20; public const int FirstPageNumber = 1; @@ -101,29 +74,13 @@ public class OpdsService : IOpdsService PublicationStatus = [] }; - public OpdsService(IUnitOfWork unitOfWork, ILocalizationService localizationService, ISeriesService seriesService, - IDownloadService downloadService, IDirectoryService directoryService, IReaderService readerService, - IEntityNamingService namingService, IReadingListService readingListService) - { - _unitOfWork = unitOfWork; - _localizationService = localizationService; - _seriesService = seriesService; - _downloadService = downloadService; - _directoryService = directoryService; - _readerService = readerService; - _namingService = namingService; - _readingListService = readingListService; - - _xmlSerializer = new XmlSerializer(typeof(Feed)); - } - - public async Task GetCatalogue(OpdsCatalogueRequest request) + public async Task GetCatalogue(OpdsCatalogueRequest request, CancellationToken ct = default) { var feed = CreateFeed("Kavita", string.Empty, request.ApiKey, request.Prefix); SetFeedId(feed, "root"); // Get the user's customized dashboard - var streams = await _unitOfWork.UserRepository.GetDashboardStreams(request.UserId, true); + var streams = await unitOfWork.UserRepository.GetDashboardStreams(request.UserId, true, ct); foreach (var stream in streams) { switch (stream.StreamType) @@ -132,10 +89,10 @@ public class OpdsService : IOpdsService feed.Entries.Add(new FeedEntry() { Id = "onDeck", - Title = await _localizationService.Translate(request.UserId, "on-deck"), + Title = await localizationService.Translate(request.UserId, "on-deck"), Content = new FeedEntryContent() { - Text = await _localizationService.Translate(request.UserId, "browse-on-deck") + Text = await localizationService.Translate(request.UserId, "browse-on-deck") }, Links = [ @@ -147,10 +104,10 @@ public class OpdsService : IOpdsService feed.Entries.Add(new FeedEntry() { Id = "recentlyAdded", - Title = await _localizationService.Translate(request.UserId, "recently-added"), + Title = await localizationService.Translate(request.UserId, "recently-added"), Content = new FeedEntryContent() { - Text = await _localizationService.Translate(request.UserId, "browse-recently-added") + Text = await localizationService.Translate(request.UserId, "browse-recently-added") }, Links = [ @@ -162,10 +119,10 @@ public class OpdsService : IOpdsService feed.Entries.Add(new FeedEntry() { Id = "recentlyUpdated", - Title = await _localizationService.Translate(request.UserId, "recently-updated"), + Title = await localizationService.Translate(request.UserId, "recently-updated"), Content = new FeedEntryContent() { - Text = await _localizationService.Translate(request.UserId, "browse-recently-updated") + Text = await localizationService.Translate(request.UserId, "browse-recently-updated") }, Links = [ @@ -174,16 +131,16 @@ public class OpdsService : IOpdsService }); break; case DashboardStreamType.MoreInGenre: - var randomGenre = await _unitOfWork.GenreRepository.GetRandomGenre(); + var randomGenre = await unitOfWork.GenreRepository.GetRandomGenre(ct); if (randomGenre == null) break; feed.Entries.Add(new FeedEntry() { Id = "moreInGenre", - Title = await _localizationService.Translate(request.UserId, "more-in-genre", randomGenre.Title), + Title = await localizationService.Translate(request.UserId, "more-in-genre", randomGenre.Title), Content = new FeedEntryContent() { - Text = await _localizationService.Translate(request.UserId, "browse-more-in-genre", randomGenre.Title) + Text = await localizationService.Translate(request.UserId, "browse-more-in-genre", randomGenre.Title) }, Links = [ @@ -214,10 +171,10 @@ public class OpdsService : IOpdsService feed.Entries.Add(new FeedEntry() { Id = "readingList", - Title = await _localizationService.Translate(request.UserId, "reading-lists"), + Title = await localizationService.Translate(request.UserId, "reading-lists"), Content = new FeedEntryContent() { - Text = await _localizationService.Translate(request.UserId, "browse-reading-lists") + Text = await localizationService.Translate(request.UserId, "browse-reading-lists") }, Links = [ @@ -227,10 +184,10 @@ public class OpdsService : IOpdsService feed.Entries.Add(new FeedEntry() { Id = "wantToRead", - Title = await _localizationService.Translate(request.UserId, "want-to-read"), + Title = await localizationService.Translate(request.UserId, "want-to-read"), Content = new FeedEntryContent() { - Text = await _localizationService.Translate(request.UserId, "browse-want-to-read") + Text = await localizationService.Translate(request.UserId, "browse-want-to-read") }, Links = [ @@ -240,10 +197,10 @@ public class OpdsService : IOpdsService feed.Entries.Add(new FeedEntry() { Id = "allLibraries", - Title = await _localizationService.Translate(request.UserId, "libraries"), + Title = await localizationService.Translate(request.UserId, "libraries"), Content = new FeedEntryContent() { - Text = await _localizationService.Translate(request.UserId, "browse-libraries") + Text = await localizationService.Translate(request.UserId, "browse-libraries") }, Links = [ @@ -253,10 +210,10 @@ public class OpdsService : IOpdsService feed.Entries.Add(new FeedEntry() { Id = "allCollections", - Title = await _localizationService.Translate(request.UserId, "collections"), + Title = await localizationService.Translate(request.UserId, "collections"), Content = new FeedEntryContent() { - Text = await _localizationService.Translate(request.UserId, "browse-collections") + Text = await localizationService.Translate(request.UserId, "browse-collections") }, Links = [ @@ -264,15 +221,15 @@ public class OpdsService : IOpdsService ] }); - if ((_unitOfWork.AppUserSmartFilterRepository.GetAllDtosByUserId(request.UserId)).Any()) + if ((await unitOfWork.AppUserSmartFilterRepository.GetAllDtosByUserId(request.UserId, ct)).Any()) { feed.Entries.Add(new FeedEntry() { Id = "allSmartFilters", - Title = await _localizationService.Translate(request.UserId, "smart-filters"), + Title = await localizationService.Translate(request.UserId, "smart-filters"), Content = new FeedEntryContent() { - Text = await _localizationService.Translate(request.UserId, "browse-smart-filters") + Text = await localizationService.Translate(request.UserId, "browse-smart-filters") }, Links = [ @@ -284,12 +241,12 @@ public class OpdsService : IOpdsService return feed; } - public async Task GetSmartFilters(OpdsPaginatedCatalogueRequest request) + public async Task GetSmartFilters(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); - var filters = await _unitOfWork.AppUserSmartFilterRepository.GetPagedDtosByUserIdAsync(userId, GetUserParams(request.PageNumber)); - var feed = CreateFeed(await _localizationService.Translate(userId, "smartFilters"), $"{apiKey}/smart-filters", apiKey, prefix); + var filters = await unitOfWork.AppUserSmartFilterRepository.GetPagedDtosByUserIdAsync(userId, GetUserParams(request.PageNumber), ct); + var feed = CreateFeed(await localizationService.Translate(userId, "smartFilters"), $"{apiKey}/smart-filters", apiKey, prefix); SetFeedId(feed, "smartFilters"); foreach (var filter in filters) @@ -311,16 +268,16 @@ public class OpdsService : IOpdsService return feed; } - public async Task GetLibraries(OpdsPaginatedCatalogueRequest request) + public async Task GetLibraries(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); - var feed = CreateFeed(await _localizationService.Translate(userId, "libraries"), $"{apiKey}/libraries", apiKey, prefix); + var feed = CreateFeed(await localizationService.Translate(userId, "libraries"), $"{apiKey}/libraries", apiKey, prefix); SetFeedId(feed, "libraries"); - // TODO: This needs pagination and the query can be optimized + // default: This needs pagination and the query can be optimized // Ensure libraries follow SideNav order - var userSideNavStreams = await _unitOfWork.UserRepository.GetSideNavStreams(userId); + var userSideNavStreams = await unitOfWork.UserRepository.GetSideNavStreams(userId, ct: ct); var libraries = userSideNavStreams.Where(s => s.StreamType == SideNavStreamType.Library) .Select(sideNavStream => sideNavStream.Library); @@ -347,14 +304,14 @@ public class OpdsService : IOpdsService return feed; } - public async Task GetWantToRead(OpdsPaginatedCatalogueRequest request) + public async Task GetWantToRead(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); - var wantToReadSeries = await _unitOfWork.SeriesRepository.GetWantToReadForUserV2Async(userId, GetUserParams(request.PageNumber), _filterV2Dto); - var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(wantToReadSeries.Select(s => s.Id)); + var wantToReadSeries = await unitOfWork.SeriesRepository.GetWantToReadForUserV2Async(userId, GetUserParams(request.PageNumber), _filterV2Dto, ct); + var seriesMetadatas = await unitOfWork.SeriesRepository.GetSeriesMetadataForIds(wantToReadSeries.Select(s => s.Id), ct); - var feed = CreateFeed(await _localizationService.Translate(userId, "want-to-read"), $"{apiKey}/want-to-read", apiKey, prefix); + var feed = CreateFeed(await localizationService.Translate(userId, "want-to-read"), $"{apiKey}/want-to-read", apiKey, prefix); SetFeedId(feed, "want-to-read"); AddPagination(feed, wantToReadSeries, $"{prefix}{apiKey}/want-to-read"); @@ -364,12 +321,12 @@ public class OpdsService : IOpdsService return feed; } - public async Task GetCollections(OpdsPaginatedCatalogueRequest request) + public async Task GetCollections(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); - var tags = await _unitOfWork.CollectionTagRepository.GetCollectionDtosPagedAsync(userId, GetUserParams(request.PageNumber), true); + var tags = await unitOfWork.CollectionTagRepository.GetCollectionDtosPagedAsync(userId, GetUserParams(request.PageNumber), true, ct); - var feed = CreateFeed(await _localizationService.Translate(userId, "collections"), $"{apiKey}/collections", apiKey, prefix); + var feed = CreateFeed(await localizationService.Translate(userId, "collections"), $"{apiKey}/collections", apiKey, prefix); SetFeedId(feed, "collections"); @@ -394,14 +351,14 @@ public class OpdsService : IOpdsService return feed; } - public async Task GetRecentlyAdded(OpdsPaginatedCatalogueRequest request) + public async Task GetRecentlyAdded(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); - var recentlyAdded = await _unitOfWork.SeriesRepository.GetRecentlyAddedV2(userId, GetUserParams(request.PageNumber), _filterV2Dto); - var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(recentlyAdded.Select(s => s.Id)); + var recentlyAdded = await unitOfWork.SeriesRepository.GetRecentlyAddedV2(userId, GetUserParams(request.PageNumber), _filterV2Dto, ct); + var seriesMetadatas = await unitOfWork.SeriesRepository.GetSeriesMetadataForIds(recentlyAdded.Select(s => s.Id), ct); - var feed = CreateFeed(await _localizationService.Translate(userId, "recently-added"), $"{apiKey}/recently-added", apiKey, prefix); + var feed = CreateFeed(await localizationService.Translate(userId, "recently-added"), $"{apiKey}/recently-added", apiKey, prefix); SetFeedId(feed, "recently-added"); AddPagination(feed, recentlyAdded, $"{prefix}{apiKey}/recently-added"); @@ -413,14 +370,14 @@ public class OpdsService : IOpdsService return feed; } - public async Task GetRecentlyUpdated(OpdsPaginatedCatalogueRequest request) + public async Task GetRecentlyUpdated(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); - var seriesDtos = (await _unitOfWork.SeriesRepository.GetRecentlyUpdatedSeries(userId, GetUserParams(request.PageNumber))).ToList(); - var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(seriesDtos.Select(s => s.SeriesId)); + var seriesDtos = (await unitOfWork.SeriesRepository.GetRecentlyUpdatedSeries(userId, GetUserParams(request.PageNumber), ct)).ToList(); + var seriesMetadatas = await unitOfWork.SeriesRepository.GetSeriesMetadataForIds(seriesDtos.Select(s => s.SeriesId), ct); - var feed = CreateFeed(await _localizationService.Translate(userId, "recently-updated"), $"{apiKey}/recently-updated", apiKey, prefix); + var feed = CreateFeed(await localizationService.Translate(userId, "recently-updated"), $"{apiKey}/recently-updated", apiKey, prefix); SetFeedId(feed, "recently-updated"); foreach (var groupedSeries in seriesDtos) @@ -441,14 +398,14 @@ public class OpdsService : IOpdsService return feed; } - public async Task GetOnDeck(OpdsPaginatedCatalogueRequest request) + public async Task GetOnDeck(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); - var pagedList = await _unitOfWork.SeriesRepository.GetOnDeck(userId, 0, GetUserParams(request.PageNumber), _filterDto); - var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(pagedList.Select(s => s.Id)); + var pagedList = await unitOfWork.SeriesRepository.GetOnDeck(userId, 0, GetUserParams(request.PageNumber), _filterDto, ct); + var seriesMetadatas = await unitOfWork.SeriesRepository.GetSeriesMetadataForIds(pagedList.Select(s => s.Id), ct); - var feed = CreateFeed(await _localizationService.Translate(userId, "on-deck"), $"{apiKey}/on-deck", apiKey, prefix); + var feed = CreateFeed(await localizationService.Translate(userId, "on-deck"), $"{apiKey}/on-deck", apiKey, prefix); SetFeedId(feed, "on-deck"); AddPagination(feed, pagedList, $"{prefix}{apiKey}/on-deck"); @@ -460,20 +417,20 @@ public class OpdsService : IOpdsService return feed; } - public async Task GetMoreInGenre(OpdsItemsFromEntityIdRequest request) + public async Task GetMoreInGenre(OpdsItemsFromEntityIdRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); var genreId = request.EntityId; - var genre = await _unitOfWork.GenreRepository.GetGenreById(genreId); + var genre = await unitOfWork.GenreRepository.GetGenreById(genreId, ct); if (genre == null) { - throw new OpdsException(await _localizationService.Translate(userId, "genre-doesnt-exist")); + throw new OpdsException(await localizationService.Translate(userId, "genre-doesnt-exist")); } - var seriesDtos = await _unitOfWork.SeriesRepository.GetMoreIn(userId, 0, genreId, GetUserParams(request.PageNumber)); - var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(seriesDtos.Select(s => s.Id)); + var seriesDtos = await unitOfWork.SeriesRepository.GetMoreIn(userId, 0, genreId, GetUserParams(request.PageNumber), ct); + var seriesMetadatas = await unitOfWork.SeriesRepository.GetSeriesMetadataForIds(seriesDtos.Select(s => s.Id), ct); - var feed = CreateFeed(await _localizationService.Translate(userId, "more-in-genre", genre.Title), $"{apiKey}/more-in-genre", apiKey, prefix); + var feed = CreateFeed(await localizationService.Translate(userId, "more-in-genre", genre.Title), $"{apiKey}/more-in-genre", apiKey, prefix); SetFeedId(feed, "more-in-genre"); AddPagination(feed, seriesDtos, $"{prefix}{apiKey}/more-in-genre"); @@ -489,24 +446,25 @@ public class OpdsService : IOpdsService /// Returns the Series matching this smart filter. /// /// + /// /// - public async Task GetSeriesFromSmartFilter(OpdsItemsFromEntityIdRequest request) + public async Task GetSeriesFromSmartFilter(OpdsItemsFromEntityIdRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); - var filter = await _unitOfWork.AppUserSmartFilterRepository.GetById(request.EntityId); + var filter = await unitOfWork.AppUserSmartFilterRepository.GetById(request.EntityId, ct); if (filter == null) { - throw new OpdsException(await _localizationService.Translate(userId, "smart-filter-doesnt-exist")); + throw new OpdsException(await localizationService.Translate(userId, "smart-filter-doesnt-exist")); } - var feed = CreateFeed(await _localizationService.Translate(userId, "smartFilters-" + filter.Id), $"{apiKey}/smart-filters/{filter.Id}/", apiKey, prefix); + var feed = CreateFeed(await localizationService.Translate(userId, "smartFilters-" + filter.Id), $"{apiKey}/smart-filters/{filter.Id}/", apiKey, prefix); SetFeedId(feed, "smartFilters-" + filter.Id); var decodedFilter = SmartFilterHelper.Decode(filter.Filter); - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, GetUserParams(request.PageNumber), - decodedFilter); - var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(series.Select(s => s.Id)); + var series = await unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, GetUserParams(request.PageNumber), + decodedFilter, ct: ct); + var seriesMetadatas = await unitOfWork.SeriesRepository.GetSeriesMetadataForIds(series.Select(s => s.Id), ct); foreach (var seriesDto in series) { @@ -518,19 +476,19 @@ public class OpdsService : IOpdsService return feed; } - public async Task GetSeriesFromCollection(OpdsItemsFromEntityIdRequest request) + public async Task GetSeriesFromCollection(OpdsItemsFromEntityIdRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); var collectionId = request.EntityId; - var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(collectionId); + var tag = await unitOfWork.CollectionTagRepository.GetCollectionAsync(collectionId, ct: ct); if (tag == null || (tag.AppUserId != userId && !tag.Promoted)) { - throw new OpdsException(await _localizationService.Translate(userId, "collection-doesnt-exist")); + throw new OpdsException(await localizationService.Translate(userId, "collection-doesnt-exist")); } - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, userId, GetUserParams(request.PageNumber)); - var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(series.Select(s => s.Id)); + var series = await unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, userId, GetUserParams(request.PageNumber), ct); + var seriesMetadatas = await unitOfWork.SeriesRepository.GetSeriesMetadataForIds(series.Select(s => s.Id), ct); var feed = CreateFeed(tag.Title + " Collection", $"{apiKey}/collections/{collectionId}", apiKey, prefix); SetFeedId(feed, $"collections-{collectionId}"); @@ -544,17 +502,17 @@ public class OpdsService : IOpdsService return feed; } - public async Task GetSeriesFromLibrary(OpdsItemsFromEntityIdRequest request) + public async Task GetSeriesFromLibrary(OpdsItemsFromEntityIdRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); var libraryId = request.EntityId; - var library = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId)) + var library = (await unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId, ct)) .SingleOrDefault(l => l.Id == libraryId); if (library == null) { - throw new OpdsException(await _localizationService.Translate(userId, "no-library-access")); + throw new OpdsException(await localizationService.Translate(userId, "no-library-access")); } var filter = new FilterV2Dto @@ -569,8 +527,8 @@ public class OpdsService : IOpdsService ] }; - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, GetUserParams(request.PageNumber), filter); - var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(series.Select(s => s.Id)); + var series = await unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, GetUserParams(request.PageNumber), filter, ct: ct); + var seriesMetadatas = await unitOfWork.SeriesRepository.GetSeriesMetadataForIds(series.Select(s => s.Id), ct); var feed = CreateFeed(library.Name, $"{apiKey}/libraries/{libraryId}", apiKey, prefix); SetFeedId(feed, $"library-{library.Name}"); @@ -583,25 +541,25 @@ public class OpdsService : IOpdsService } - public async Task GetReadingListItems(OpdsItemsFromEntityIdRequest request) + public async Task GetReadingListItems(OpdsItemsFromEntityIdRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out _); var readingListId = request.EntityId; - var readingList = await _unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, userId); + var readingList = await unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, userId, ct); if (readingList == null) { - throw new OpdsException(await _localizationService.Translate(request.UserId, "reading-list-restricted")); + throw new OpdsException(await localizationService.Translate(request.UserId, "reading-list-restricted")); } - var feed = CreateFeed(readingList.Title + " " + await _localizationService.Translate(userId, "reading-list"), $"{apiKey}/reading-list/{readingListId}", apiKey, prefix); + var feed = CreateFeed(readingList.Title + " " + await localizationService.Translate(userId, "reading-list"), $"{apiKey}/reading-list/{readingListId}", apiKey, prefix); SetFeedId(feed, $"reading-list-{readingListId}"); - var items = await _readingListService.GetReadingListItems(readingListId, userId, GetUserParams(request.PageNumber)); - var totalItems = await _unitOfWork.ReadingListRepository .GetReadingListItemCountAsync(readingListId, userId); + var items = await readingListService.GetReadingListItems(readingListId, userId, GetUserParams(request.PageNumber)); + var totalItems = await unitOfWork.ReadingListRepository .GetReadingListItemCountAsync(readingListId, userId, ct); var chapterIds = items.Select(i => i.ChapterId).Distinct().ToList(); - var chapters = (await _unitOfWork.ChapterRepository .GetChapterDtosAsync(chapterIds, userId)) + var chapters = (await unitOfWork.ChapterRepository .GetChapterDtosAsync(chapterIds, userId, ct)) .ToDictionary(c => c.Id); // Build naming contexts per library type (usually just 1-2) @@ -613,15 +571,15 @@ public class OpdsService : IOpdsService if (request.Preferences.IncludeContinueFrom && request.PageNumber == FirstPageNumber) { - var anyProgress = await _unitOfWork.ReadingListRepository.AnyUserReadingProgressAsync(readingListId, userId); + var anyProgress = await unitOfWork.ReadingListRepository.AnyUserReadingProgressAsync(readingListId, userId, ct); if (anyProgress) { - var continuePoint = await _unitOfWork.ReadingListRepository.GetContinueReadingPoint(readingListId, userId); + var continuePoint = await unitOfWork.ReadingListRepository.GetContinueReadingPoint(readingListId, userId, ct); if (continuePoint != null) { var continueChapter = - await _unitOfWork.ChapterRepository.GetChapterDtoAsync(continuePoint.ChapterId, request.UserId); + await unitOfWork.ChapterRepository.GetChapterDtoAsync(continuePoint.ChapterId, request.UserId, ct); if (continueChapter is {Files.Count: 1}) { feed.Entries.Add(await CreateContinueReadingEntryAsync(continuePoint, continueChapter, request)); @@ -669,32 +627,32 @@ public class OpdsService : IOpdsService foreach (var libraryType in libraryTypes.Distinct()) { contexts[libraryType] = await LocalizedNamingContext.CreateAsync( - _namingService, _localizationService, userId, libraryType); + namingService, localizationService, userId, libraryType); } return contexts; } - public async Task GetSeriesDetail(OpdsItemsFromEntityIdRequest request) + public async Task GetSeriesDetail(OpdsItemsFromEntityIdRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); var seriesId = request.EntityId; - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); + var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId, ct); if (series == null) { - throw new OpdsException(await _localizationService.Translate(userId, "series-doesnt-exist")); + throw new OpdsException(await localizationService.Translate(userId, "series-doesnt-exist")); } - var seriesDetailTask = _seriesService.GetSeriesDetail(seriesId, userId); - var libraryTypeTask = _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId); + var seriesDetailTask = seriesService.GetSeriesDetail(seriesId, userId); + var libraryTypeTask = unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId, ct); await Task.WhenAll(seriesDetailTask, libraryTypeTask); var seriesDetail = await seriesDetailTask; var libraryType = await libraryTypeTask; - var namingContext = await LocalizedNamingContext.CreateAsync(_namingService, _localizationService, userId, libraryType); + var namingContext = await LocalizedNamingContext.CreateAsync(namingService, localizationService, userId, libraryType); var volumesById = seriesDetail.Volumes.ToDictionary(v => v.Id); var feed = CreateFeed(series.Name + " - Storyline", $"{apiKey}/series/{series.Id}", apiKey, prefix); @@ -705,11 +663,11 @@ public class OpdsService : IOpdsService // Check if there is reading progress or not, if so, inject a "continue-reading" item if (request.Preferences.IncludeContinueFrom) { - var anyUserProgress = await _unitOfWork.AppUserProgressRepository - .AnyUserProgressForSeriesAsync(seriesId, userId); + var anyUserProgress = await unitOfWork.AppUserProgressRepository + .AnyUserProgressForSeriesAsync(seriesId, userId, ct); if (anyUserProgress) { - var continueChapter = await _readerService.GetContinuePoint(seriesId, userId); + var continueChapter = await readerService.GetContinuePoint(seriesId, userId); if (continueChapter is { Files.Count: 1 }) { volumesById.TryGetValue(continueChapter.VolumeId, out var continueVolume); @@ -770,26 +728,27 @@ public class OpdsService : IOpdsService return feed; } - public async Task GetItemsFromVolume(OpdsItemsFromCompoundEntityIdsRequest request) + public async Task GetItemsFromVolume(OpdsItemsFromCompoundEntityIdsRequest request, + CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out _); var seriesId = request.SeriesId; var volumeId = request.VolumeId; - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); + var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId, ct); if (series == null) { - throw new OpdsException(await _localizationService.Translate(userId, "series-doesnt-exist")); + throw new OpdsException(await localizationService.Translate(userId, "series-doesnt-exist")); } - var volume = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, request.UserId); + var volume = await unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, request.UserId, ct); if (volume == null) { - throw new OpdsException(await _localizationService.Translate(userId, "volume-doesnt-exist")); + throw new OpdsException(await localizationService.Translate(userId, "volume-doesnt-exist")); } - var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId); - var namingContext = await LocalizedNamingContext.CreateAsync( _namingService, _localizationService, userId, libraryType); + var libraryType = await unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId, ct); + var namingContext = await LocalizedNamingContext.CreateAsync( namingService, localizationService, userId, libraryType); var feed = CreateFeed($"{series.Name} - Volume {volume.Name}", $"{apiKey}/series/{seriesId}/volume/{volumeId}", apiKey, prefix); @@ -818,7 +777,8 @@ public class OpdsService : IOpdsService return feed; } - public async Task GetItemsFromChapter(OpdsItemsFromCompoundEntityIdsRequest request) + public async Task GetItemsFromChapter(OpdsItemsFromCompoundEntityIdsRequest request, + CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out _); @@ -826,26 +786,26 @@ public class OpdsService : IOpdsService var volumeId = request.VolumeId; var chapterId = request.ChapterId; - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); + var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId, ct); if (series == null) { - throw new OpdsException(await _localizationService.Translate(userId, "series-doesnt-exist")); + throw new OpdsException(await localizationService.Translate(userId, "series-doesnt-exist")); } - var volume = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, userId); + var volume = await unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, userId, ct); if (volume == null) { - throw new OpdsException(await _localizationService.Translate(userId, "volume-doesnt-exist")); + throw new OpdsException(await localizationService.Translate(userId, "volume-doesnt-exist")); } - var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId); + var libraryType = await unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId, ct); var chapter = volume.Chapters.FirstOrDefault(c => c.Id == chapterId); if (chapter == null) { - throw new OpdsException(await _localizationService.Translate(userId, "chapter-doesnt-exist")); + throw new OpdsException(await localizationService.Translate(userId, "chapter-doesnt-exist")); } - var namingContext = await LocalizedNamingContext.CreateAsync(_namingService, _localizationService, userId, libraryType); + var namingContext = await LocalizedNamingContext.CreateAsync(namingService, localizationService, userId, libraryType); var chapterName = namingContext.FormatChapterTitle(chapter); var feed = CreateFeed( $"{series.Name} - Volume {volume.Name} - {chapterName} {chapterId}", @@ -861,29 +821,29 @@ public class OpdsService : IOpdsService return feed; } - public async Task Search(OpdsSearchRequest request) + public async Task Search(OpdsSearchRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); var query = request.Query; - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, ct: ct); if (string.IsNullOrEmpty(query)) { - throw new OpdsException(await _localizationService.Translate(userId, "query-required")); + throw new OpdsException(await localizationService.Translate(userId, "query-required")); } query = query.Replace("%", string.Empty); - var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId)).ToList(); + var libraries = (await unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId, ct)).ToList(); if (libraries.Count == 0) { - throw new OpdsException(await _localizationService.Translate(userId, "libraries-restricted")); + throw new OpdsException(await localizationService.Translate(userId, "libraries-restricted")); } - var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user); + var isAdmin = await unitOfWork.UserRepository.IsUserAdminAsync(user, ct); - var searchResults = await _unitOfWork.SeriesRepository.SearchSeries(userId, isAdmin, - libraries.Select(l => l.Id).ToArray(), query, includeChapterAndFiles: false); + var searchResults = await unitOfWork.SeriesRepository.SearchSeries(userId, isAdmin, + libraries.Select(l => l.Id).ToArray(), query, includeChapterAndFiles: false, ct: ct); var feed = CreateFeed(query, $"{apiKey}/series?query=" + query, apiKey, prefix); SetFeedId(feed, "search-series"); @@ -930,12 +890,12 @@ public class OpdsService : IOpdsService return feed; } - public async Task GetReadingLists(OpdsPaginatedCatalogueRequest request) + public async Task GetReadingLists(OpdsPaginatedCatalogueRequest request, CancellationToken ct = default) { var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl); - var readingLists = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(userId, - true, GetUserParams(request.PageNumber), false); + var readingLists = await unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(userId, + true, GetUserParams(request.PageNumber), false, ct); var feed = CreateFeed("All Reading Lists", $"{apiKey}/reading-list", apiKey, prefix); @@ -1086,7 +1046,7 @@ public class OpdsService : IOpdsService feed.Links.Add(CreateLink(FeedLinkRelation.Next, FeedLinkType.AtomNavigation, url + "pageNumber=" + (pageNumber + 1))); } - // Update self to point to current page + // Update self to point to the current page var selfLink = feed.Links.SingleOrDefault(l => l.Rel == FeedLinkRelation.Self); if (selfLink != null) { @@ -1124,7 +1084,7 @@ public class OpdsService : IOpdsService feed.Links.Add(CreateLink(FeedLinkRelation.Next, FeedLinkType.AtomNavigation, url + "pageNumber=" + (pageNumber + 1))); } - // Update self to point to current page + // Update self to point to the current page var selfLink = feed.Links.SingleOrDefault(l => l.Rel == FeedLinkRelation.Self); if (selfLink != null) { @@ -1219,7 +1179,7 @@ public class OpdsService : IOpdsService { var mangaFile = chapter.Files.First(); var fileSize = GetFileSize(mangaFile); - var fileType = _downloadService.GetContentTypeFromFile(mangaFile.FilePath); + var fileType = downloadService.GetContentTypeFromFile(mangaFile.FilePath); var filename = Uri.EscapeDataString(Path.GetFileName(mangaFile.FilePath)); @@ -1273,7 +1233,7 @@ public class OpdsService : IOpdsService { var fileSize = mangaFile.Bytes > 0 ? DirectoryService.GetHumanReadableBytes(mangaFile.Bytes) : - DirectoryService.GetHumanReadableBytes(_directoryService.GetTotalSize((List) [mangaFile.FilePath])); + DirectoryService.GetHumanReadableBytes(directoryService.GetTotalSize((List) [mangaFile.FilePath])); return fileSize; } @@ -1292,10 +1252,10 @@ public class OpdsService : IOpdsService { var mangaFile = chapter.Files.First(); var fileSize = GetFileSize(mangaFile); - var fileType = _downloadService.GetContentTypeFromFile(mangaFile.FilePath); + var fileType = downloadService.GetContentTypeFromFile(mangaFile.FilePath); var filename = Uri.EscapeDataString(Path.GetFileName(mangaFile.FilePath)); - var title = _namingService.FormatReadingListItemTitle(item); + var title = namingService.FormatReadingListItemTitle(item); var displayTitle = $"{item.Order} - {item.SeriesName}: {title}"; var accLink = CreateLink( @@ -1391,13 +1351,13 @@ public class OpdsService : IOpdsService } /// - /// Creates a continue reading feed entry from a chapter. + /// Creates a continued reading feed entry from a chapter. /// private async Task CreateContinueReadingEntryAsync( SeriesDto series, VolumeDto? volume, ChapterDto chapter, LocalizedNamingContext namingContext, IOpdsRequest request) { var entry = CreateChapterWithFile(series, volume, chapter, namingContext, request); - entry.Title = await _localizationService.Translate( + entry.Title = await localizationService.Translate( request.UserId, "opds-continue-reading-title", entry.Title); return entry; @@ -1414,7 +1374,7 @@ public class OpdsService : IOpdsService ? entry.Title[2..] : entry.Title; - entry.Title = await _localizationService.Translate( + entry.Title = await localizationService.Translate( request.UserId, "opds-continue-reading-title", titleWithoutIcon); return entry; diff --git a/API/Services/PersonService.cs b/Kavita.Services/PersonService.cs similarity index 75% rename from API/Services/PersonService.cs rename to Kavita.Services/PersonService.cs index ff0049cbe..63ae7cb38 100644 --- a/API/Services/PersonService.cs +++ b/Kavita.Services/PersonService.cs @@ -1,38 +1,19 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.Entities.Person; -using API.Extensions; -using API.Helpers.Builders; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.Common.Extensions; +using Kavita.Models.Builders; +using Kavita.Models.Entities.Person; -namespace API.Services; - -public interface IPersonService -{ - /// - /// Adds src as an alias to dst, this is a destructive operation - /// - /// Merged person - /// Remaining person - /// The entities passed as arguments **must** include all relations - /// - Task MergePeopleAsync(Person src, Person dst); - - /// - /// Adds the alias to the person, requires that the aliases are not shared with anyone else - /// - /// This method does NOT commit changes - /// - /// - /// - Task UpdatePersonAliasesAsync(Person person, IList aliases); -} +namespace Kavita.Services; public class PersonService(IUnitOfWork unitOfWork): IPersonService { - public async Task MergePeopleAsync(Person src, Person dst) + public async Task MergePeopleAsync(Person src, Person dst, CancellationToken ct = default) { if (dst.Id == src.Id) return; @@ -78,7 +59,7 @@ public class PersonService(IUnitOfWork unitOfWork): IPersonService unitOfWork.PersonRepository.Remove(src); unitOfWork.PersonRepository.Update(dst); - await unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); } private static void MergeChapterPeople(Person dst, Person src) @@ -122,7 +103,7 @@ public class PersonService(IUnitOfWork unitOfWork): IPersonService } } - public async Task UpdatePersonAliasesAsync(Person person, IList aliases) + public async Task UpdatePersonAliasesAsync(Person person, IList aliases, CancellationToken ct = default) { var normalizedAliases = aliases .Select(a => a.ToNormalized()) @@ -135,7 +116,7 @@ public class PersonService(IUnitOfWork unitOfWork): IPersonService return true; } - var others = await unitOfWork.PersonRepository.GetPeopleByNames(normalizedAliases); + var others = await unitOfWork.PersonRepository.GetPeopleByNames(normalizedAliases, ct: ct); others = others.Where(p => p.Id != person.Id).ToList(); if (others.Count != 0) return false; diff --git a/API/Services/Plus/ExternalMetadataService.cs b/Kavita.Services/Plus/ExternalMetadataService.cs similarity index 90% rename from API/Services/Plus/ExternalMetadataService.cs rename to Kavita.Services/Plus/ExternalMetadataService.cs index 0010bca82..2c841e206 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/Kavita.Services/Plus/ExternalMetadataService.cs @@ -2,61 +2,43 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Collection; -using API.DTOs.KavitaPlus.ExternalMetadata; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Metadata.Matching; -using API.DTOs.Person; -using API.DTOs.Recommendation; -using API.DTOs.Scrobbling; -using API.DTOs.SeriesDetail; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Interfaces; -using API.Entities.Metadata; -using API.Entities.MetadataMatching; -using API.Extensions; -using API.Helpers; -using API.Helpers.Builders; -using API.Services.Tasks.Metadata; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; using AutoMapper; using Flurl.Http; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services.Metadata; +using Kavita.API.Services.Plus; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Common.Extensions; using Kavita.Common.Helpers; +using Kavita.Models.Builders; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Collection; +using Kavita.Models.DTOs.KavitaPlus.ExternalMetadata; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Metadata.Matching; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.Recommendation; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Interfaces; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.MetadataMatching; +using Kavita.Models.Entities.User; +using Kavita.Models.Extensions; +using Kavita.Services.Extensions; +using Kavita.Services.Helpers; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; -namespace API.Services.Plus; -#nullable enable - - - -public interface IExternalMetadataService -{ - Task GetExternalSeriesDetail(int? aniListId, long? malId, int? seriesId); - Task GetSeriesDetailPlus(int seriesId, LibraryType libraryType); - 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. - /// - /// - /// - /// If the fetch was made - Task FetchSeriesMetadata(int seriesId, LibraryType libraryType); - - Task> GetStacksForUser(int userId); - Task> MatchSeries(MatchSeriesDto dto); - Task FixSeriesMatch(int seriesId, int? aniListId, long? malId, int? cbrId); - Task UpdateSeriesDontMatch(int seriesId, bool dontMatch); - Task WriteExternalMetadataToSeries(ExternalSeriesDetailDto externalMetadata, int seriesId); -} +namespace Kavita.Services.Plus; public class ExternalMetadataService : IExternalMetadataService { @@ -108,21 +90,15 @@ public class ExternalMetadataService : IExternalMetadataService 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 25 records. The goal is to slowly build out/refresh the data - /// [DisableConcurrentExecution(60 * 60 * 60)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - public async Task FetchExternalDataTask() + public async Task FetchExternalDataTask(CancellationToken ct = default) { // Find all Series that are eligible and limit - var ids = await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesThatNeedExternalMetadata(25); + var ids = await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesThatNeedExternalMetadata(25, ct: ct); if (ids.Count == 0) { - ids = await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesThatNeedExternalMetadata(25, true); + ids = await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesThatNeedExternalMetadata(25, true, ct); } if (ids.Count == 0) @@ -135,32 +111,26 @@ public class ExternalMetadataService : IExternalMetadataService _logger.LogInformation("[Kavita+ Data Refresh] Started Refreshing {Count} series data from Kavita+: {Ids}", ids.Count, string.Join(',', ids)); var count = 0; var successfulMatches = new List(); - var libTypes = await _unitOfWork.LibraryRepository.GetLibraryTypesBySeriesIdsAsync(ids); + var libTypes = await _unitOfWork.LibraryRepository.GetLibraryTypesBySeriesIdsAsync(ids, ct); foreach (var seriesId in ids) { var libraryType = libTypes[seriesId]; - var success = await FetchSeriesMetadata(seriesId, libraryType); + var success = await FetchSeriesMetadata(seriesId, libraryType, ct); if (success) { count++; successfulMatches.Add(seriesId); } - await Task.Delay(10000); // Currently AL is degraded and has 30 requests/min, give a little padding since this is a background request + await Task.Delay(10000, ct); // Currently AL is degraded and has 30 requests/min, give a little padding since this is a background request } _logger.LogInformation("[Kavita+ Data Refresh] Finished Refreshing {Count} / {Total} series data from Kavita+: {Ids}", count, ids.Count, string.Join(',', successfulMatches)); } - /// - /// Fetches data from Kavita+ - /// - /// - /// - /// If a successful match was made - public async Task FetchSeriesMetadata(int seriesId, LibraryType libraryType) + public async Task FetchSeriesMetadata(int seriesId, LibraryType libraryType, CancellationToken ct = default) { if (!IsPlusEligible(libraryType)) return false; - if (!await _licenseService.HasActiveLicense()) return false; + if (!await _licenseService.HasActiveLicense(ct: ct)) return false; // 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 @@ -172,15 +142,15 @@ public class ExternalMetadataService : IExternalMetadataService } // Prefetch SeriesDetail data - return await GetSeriesDetailPlus(seriesId, libraryType) != null; + return await GetSeriesDetailPlus(seriesId, libraryType, ct) != null; } - public async Task> GetStacksForUser(int userId) + public async Task> GetStacksForUser(int userId, CancellationToken ct = default) { - if (!await _licenseService.HasActiveLicense()) return ArraySegment.Empty; + if (!await _licenseService.HasActiveLicense(ct: ct)) return ArraySegment.Empty; // See if this user has Mal account on record - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, ct: ct); if (user == null || string.IsNullOrEmpty(user.MalUserName) || string.IsNullOrEmpty(user.MalAccessToken)) { _logger.LogInformation("User is attempting to fetch MAL Stacks, but missing information on their account"); @@ -190,8 +160,8 @@ public class ExternalMetadataService : IExternalMetadataService { _logger.LogDebug("Fetching Kavita+ for MAL Stacks for user {UserName}", user.MalUserName); - var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - return await _kavitaPlusApiService.GetMalStacks(user.MalUserName, license); + var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct)).Value; + return await _kavitaPlusApiService.GetMalStacks(user.MalUserName, license, ct); } catch (Exception ex) { @@ -200,23 +170,15 @@ public class ExternalMetadataService : IExternalMetadataService } } - /// - /// Returns the match results for a Series from UI Flow - /// - /// - /// Will extract alternative names like Localized name, year will send as ReleaseYear but fallback to Comic Vine syntax if applicable - /// - /// - /// - public async Task> MatchSeries(MatchSeriesDto dto) + public async Task> MatchSeries(MatchSeriesDto dto, CancellationToken ct = default) { var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, - SeriesIncludes.Metadata | SeriesIncludes.ExternalMetadata | SeriesIncludes.Library); + SeriesIncludes.Metadata | SeriesIncludes.ExternalMetadata | SeriesIncludes.Library, ct); if (series == null) return []; - var potentialAnilistId = ScrobblingService.ExtractId(dto.Query, ScrobblingService.AniListWeblinkWebsite); - var potentialMalId = ScrobblingService.ExtractId(dto.Query, ScrobblingService.MalWeblinkWebsite); + var potentialAnilistId = ScrobblingHelper.ExtractId(dto.Query, ScrobblingService.AniListWeblinkWebsite); + var potentialMalId = ScrobblingHelper.ExtractId(dto.Query, ScrobblingService.MalWeblinkWebsite); var format = series.Library.Type.ConvertToPlusMediaFormat(series.Format); var otherNames = ExtractAlternativeNames(series); @@ -238,13 +200,13 @@ public class ExternalMetadataService : IExternalMetadataService SeriesName = series.Name, AlternativeNames = otherNames, Year = year, - AniListId = potentialAnilistId ?? ScrobblingService.GetAniListId(series), - MalId = potentialMalId ?? ScrobblingService.GetMalId(series) + AniListId = potentialAnilistId ?? ScrobblingHelper.GetAniListId(series), + MalId = potentialMalId ?? ScrobblingHelper.GetMalId(series) }; try { - var results = await _kavitaPlusApiService.MatchSeries(matchRequest); + var results = await _kavitaPlusApiService.MatchSeries(matchRequest, ct); // Some summaries can contain multiple
s, we need to ensure it's only 1 foreach (var result in results) @@ -269,15 +231,7 @@ public class ExternalMetadataService : IExternalMetadataService } - /// - /// Retrieves Metadata about a Recommended External Series - /// - /// - /// - /// - /// - /// - public async Task GetExternalSeriesDetail(int? aniListId, long? malId, int? seriesId) + public async Task GetExternalSeriesDetail(int? aniListId, long? malId, int? seriesId, CancellationToken ct = default) { if (!aniListId.HasValue && !malId.HasValue) { @@ -285,42 +239,34 @@ public class ExternalMetadataService : IExternalMetadataService } // This is for the Series drawer. We can get this extra information during the initial SeriesDetail call so it's all coming from the DB - var details = await GetSeriesDetail(aniListId, malId, seriesId); - - return details; + return await GetSeriesDetail(aniListId, malId, seriesId, ct); } - /// - /// Returns Series Detail data from Kavita+ - Review, Recs, Ratings - /// - /// - /// - /// - public async Task GetSeriesDetailPlus(int seriesId, LibraryType libraryType) + public async Task GetSeriesDetailPlus(int seriesId, LibraryType libraryType, CancellationToken ct = default) { - if (!IsPlusEligible(libraryType) || !await _licenseService.HasActiveLicense()) return _defaultReturn; + if (!IsPlusEligible(libraryType) || !await _licenseService.HasActiveLicense(ct: ct)) return _defaultReturn; // Check blacklist (bad matches) or if there is a don't match - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, ct: ct); if (series == null || !series.WillScrobble()) return _defaultReturn; var needsRefresh = - await _unitOfWork.ExternalSeriesMetadataRepository.NeedsDataRefresh(seriesId); + await _unitOfWork.ExternalSeriesMetadataRepository.NeedsDataRefresh(seriesId, ct); if (!needsRefresh) { // Convert into DTOs and return - return await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesDetailPlusDto(seriesId); + return await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesDetailPlusDto(seriesId, ct); } - var data = await _unitOfWork.SeriesRepository.GetPlusSeriesDto(seriesId); + var data = await _unitOfWork.SeriesRepository.GetPlusSeriesDto(seriesId, ct); if (data == null) return _defaultReturn; // Get from Kavita+ API the Full Series metadata with rec/rev and cache to ExternalMetadata tables try { - return await FetchExternalMetadataForSeries(seriesId, libraryType, data); + return await FetchExternalMetadataForSeries(seriesId, libraryType, data, ct); } catch (KavitaException ex) { @@ -330,16 +276,9 @@ public class ExternalMetadataService : IExternalMetadataService } } - /// - /// This will override any sort of matching that was done prior and force it to be what the user Selected - /// - /// - /// - /// - /// - public async Task FixSeriesMatch(int seriesId, int? aniListId, long? malId, int? cbrId) + public async Task FixSeriesMatch(int seriesId, int? aniListId, long? malId, int? cbrId, CancellationToken ct = default) { - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library, ct); if (series == null) return; // Remove from Blacklist @@ -358,7 +297,7 @@ public class ExternalMetadataService : IExternalMetadataService CbrId = cbrId, MediaFormat = series.Library.Type.ConvertToPlusMediaFormat(series.Format), SeriesName = series.Name // Required field, not used since AniList/Mal Id are passed - }); + }, ct); if (metadata.Series == null) { @@ -368,14 +307,14 @@ public class ExternalMetadataService : IExternalMetadataService } // Find all scrobble events and rewrite them to be the correct - var events = await _unitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId); + var events = await _unitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId, ct); _unitOfWork.ScrobbleRepository.Remove(events); // Find all scrobble errors and remove them - var errors = await _unitOfWork.ScrobbleRepository.GetAllScrobbleErrorsForSeries(seriesId); + var errors = await _unitOfWork.ScrobbleRepository.GetAllScrobbleErrorsForSeries(seriesId, ct); _unitOfWork.ScrobbleRepository.Remove(errors); - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); // Regenerate all events for the series for all users BackgroundJob.Enqueue(() => _scrobblingService.CreateEventsFromExistingHistoryForSeries(seriesId)); @@ -388,20 +327,14 @@ public class ExternalMetadataService : IExternalMetadataService { // We can't rethrow because Fix match is done in a background thread and Hangfire will requeue multiple times _logger.LogInformation(ex, "Rate limit hit for matching {SeriesName} with Kavita+", series.Name); - // Fire SignalR event about this await _eventHub.SendMessageAsync(MessageFactory.ExternalMatchRateLimitError, - MessageFactory.ExternalMatchRateLimitErrorEvent(series.Id, series.Name)); + MessageFactory.ExternalMatchRateLimitErrorEvent(series.Id, series.Name), ct: ct); } } - /// - /// Sets a series to Don't Match and removes all previously cached - /// - /// - /// - public async Task UpdateSeriesDontMatch(int seriesId, bool dontMatch) + public async Task UpdateSeriesDontMatch(int seriesId, bool dontMatch, CancellationToken ct = default) { - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.ExternalMetadata); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.ExternalMetadata, ct); if (series == null) return; _logger.LogInformation("User has asked Kavita to stop matching/scrobbling on {SeriesName}", series.Name); @@ -420,7 +353,7 @@ public class ExternalMetadataService : IExternalMetadataService _unitOfWork.SeriesRepository.Update(series); - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); } /// @@ -430,10 +363,10 @@ public class ExternalMetadataService : IExternalMetadataService /// /// /// - private async Task FetchExternalMetadataForSeries(int seriesId, LibraryType libraryType, PlusSeriesRequestDto data) + private async Task FetchExternalMetadataForSeries(int seriesId, LibraryType libraryType, PlusSeriesRequestDto data, CancellationToken ct = default) { - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library, ct); if (series == null) { return _defaultReturn; @@ -447,7 +380,7 @@ public class ExternalMetadataService : IExternalMetadataService try { // This returns an AniListSeries and Match returns ExternalSeriesDto - result = await _kavitaPlusApiService.GetSeriesDetail(data); + result = await _kavitaPlusApiService.GetSeriesDetail(data, ct); } catch (FlurlHttpException ex) { @@ -460,14 +393,14 @@ public class ExternalMetadataService : IExternalMetadataService if (errorMessage.Contains("Too many Requests")) { _logger.LogDebug("Hit rate limit, will retry in 3 seconds"); - await Task.Delay(3000); + await Task.Delay(3000, ct); - result = await _kavitaPlusApiService.GetSeriesDetail(data); + result = await _kavitaPlusApiService.GetSeriesDetail(data, ct); } else if (errorMessage.Contains("Unknown Series")) { series.IsBlacklisted = true; - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); } } } @@ -522,11 +455,11 @@ public class ExternalMetadataService : IExternalMetadataService var madeMetadataModification = false; if (result.Series != null && series.Library.AllowMetadataMatching) { - externalSeriesMetadata.Series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); + externalSeriesMetadata.Series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, ct: ct); try { - madeMetadataModification = await WriteExternalMetadataToSeries(result.Series, seriesId); + madeMetadataModification = await WriteExternalMetadataToSeries(result.Series, seriesId, ct); if (madeMetadataModification) { _unitOfWork.SeriesRepository.Update(series); @@ -543,13 +476,13 @@ public class ExternalMetadataService : IExternalMetadataService // WriteExternalMetadataToSeries will commit but not always if (_unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); } if (madeMetadataModification) { // Inform the UI of the update - await _eventHub.SendMessageAsync(MessageFactory.ScanSeries, MessageFactory.ScanSeriesEvent(series.LibraryId, series.Id, series.Name), false); + await _eventHub.SendMessageAsync(MessageFactory.ScanSeries, MessageFactory.ScanSeriesEvent(series.LibraryId, series.Id, series.Name), false, ct); } return new SeriesDetailPlusDto() @@ -588,26 +521,20 @@ public class ExternalMetadataService : IExternalMetadataService // Blacklist the series as it wasn't found in Kavita+ series.IsBlacklisted = true; - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); return _defaultReturn; } - /// - /// Given external metadata from Kavita+, write as much as possible to the Kavita series as possible - /// - /// - /// - /// - public async Task WriteExternalMetadataToSeries(ExternalSeriesDetailDto externalMetadata, int seriesId) + public async Task WriteExternalMetadataToSeries(ExternalSeriesDetailDto externalMetadata, int seriesId, CancellationToken ct = default) { - var settings = await _unitOfWork.SettingsRepository.GetMetadataSettingDto(); + var settings = await _unitOfWork.SettingsRepository.GetMetadataSettingDto(ct); if (!settings.Enabled) return false; - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Related); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Related, ct); if (series == null) return false; - var defaultAdmin = await _unitOfWork.UserRepository.GetDefaultAdminUser(); + var defaultAdmin = await _unitOfWork.UserRepository.GetDefaultAdminUser(ct: ct); _logger.LogInformation("Writing External metadata to Series {SeriesName}", series.Name); @@ -848,7 +775,7 @@ public class ExternalMetadataService : IExternalMetadataService .Select(w => new PersonDto() { Name = w.Name.Trim(), - AniListId = ScrobblingService.ExtractId(w.Url, ScrobblingService.AniListCharacterWebsite), + AniListId = ScrobblingHelper.ExtractId(w.Url, ScrobblingService.AniListCharacterWebsite), Description = StringHelper.CorrectUrls(StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description))), }) .Concat(series.Metadata.People @@ -890,7 +817,7 @@ public class ExternalMetadataService : IExternalMetadataService foreach (var character in externalCharacters) { - var aniListId = ScrobblingService.ExtractId(character.Url, ScrobblingService.AniListCharacterWebsite); + var aniListId = ScrobblingHelper.ExtractId(character.Url, ScrobblingService.AniListCharacterWebsite); if (aniListId <= 0) continue; var person = await _unitOfWork.PersonRepository.GetPersonByAniListId(aniListId); if (person != null && !string.IsNullOrEmpty(character.ImageUrl) && string.IsNullOrEmpty(person.CoverImage)) @@ -929,7 +856,7 @@ public class ExternalMetadataService : IExternalMetadataService .Select(w => new PersonDto() { Name = w.Name.Trim(), - AniListId = ScrobblingService.ExtractId(w.Url, ScrobblingService.AniListStaffWebsite), + AniListId = ScrobblingHelper.ExtractId(w.Url, ScrobblingService.AniListStaffWebsite), Description = StringHelper.CorrectUrls(StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description))), }) .Concat(series.Metadata.People @@ -986,7 +913,7 @@ public class ExternalMetadataService : IExternalMetadataService .Select(w => new PersonDto() { Name = w.Name.Trim(), - AniListId = ScrobblingService.ExtractId(w.Url, ScrobblingService.AniListStaffWebsite), + AniListId = ScrobblingHelper.ExtractId(w.Url, ScrobblingService.AniListStaffWebsite), Description = StringHelper.CorrectUrls(StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description))), }) .Concat(series.Metadata.People @@ -1612,7 +1539,7 @@ public class ExternalMetadataService : IExternalMetadataService { foreach (var staff in people) { - var aniListId = ScrobblingService.ExtractId(staff.Url, ScrobblingService.AniListStaffWebsite); + var aniListId = ScrobblingHelper.ExtractId(staff.Url, ScrobblingService.AniListStaffWebsite); if (aniListId is null or <= 0) continue; var person = await _unitOfWork.PersonRepository.GetPersonByAniListId(aniListId.Value); if (person == null || string.IsNullOrEmpty(staff.ImageUrl) || @@ -1858,8 +1785,8 @@ public class ExternalMetadataService : IExternalMetadataService { // 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)); + libraryType, ScrobblingHelper.CreateUrl(ScrobblingService.AniListWeblinkWebsite, rec.AniListId), + ScrobblingHelper.CreateUrl(ScrobblingService.MalWeblinkWebsite, rec.MalId)); if (seriesForRec != null) { @@ -1916,8 +1843,9 @@ public class ExternalMetadataService : IExternalMetadataService /// /// /// + /// /// - private async Task GetSeriesDetail(int? aniListId, long? malId, int? seriesId) + private async Task GetSeriesDetail(int? aniListId, long? malId, int? seriesId, CancellationToken ct = default) { var payload = new ExternalMetadataIdsDto() { @@ -1930,16 +1858,16 @@ public class ExternalMetadataService : IExternalMetadataService if (seriesId is > 0) { var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId.Value, - SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.ExternalReviews); + SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.ExternalReviews, ct); if (series != null) { if (payload.AniListId <= 0) { - payload.AniListId = ScrobblingService.ExtractId(series.Metadata.WebLinks, ScrobblingService.AniListWeblinkWebsite); + payload.AniListId = ScrobblingHelper.ExtractId(series.Metadata.WebLinks, ScrobblingService.AniListWeblinkWebsite); } if (payload.MalId <= 0) { - payload.MalId = ScrobblingService.ExtractId(series.Metadata.WebLinks, ScrobblingService.MalWeblinkWebsite); + payload.MalId = ScrobblingHelper.ExtractId(series.Metadata.WebLinks, ScrobblingService.MalWeblinkWebsite); } payload.SeriesName = series.Name; payload.LocalizedSeriesName = series.LocalizedName; @@ -1949,7 +1877,7 @@ public class ExternalMetadataService : IExternalMetadataService } try { - var ret = await _kavitaPlusApiService.GetSeriesDetailById(payload); + var ret = await _kavitaPlusApiService.GetSeriesDetailById(payload, ct); ret.Summary = StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(ret.Summary)); diff --git a/API/Services/Plus/KavitaPlusApiService.cs b/Kavita.Services/Plus/KavitaPlusApiService.cs similarity index 69% rename from API/Services/Plus/KavitaPlusApiService.cs rename to Kavita.Services/Plus/KavitaPlusApiService.cs index ec4f414c3..6aa3f64db 100644 --- a/API/Services/Plus/KavitaPlusApiService.cs +++ b/Kavita.Services/Plus/KavitaPlusApiService.cs @@ -1,95 +1,86 @@ #nullable enable using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.DTOs.Collection; -using API.DTOs.KavitaPlus.ExternalMetadata; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Metadata.Matching; -using API.DTOs.Scrobbling; -using API.Entities.Enums; -using API.Extensions; using Flurl.Http; +using Kavita.API.Database; +using Kavita.API.Services.Plus; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs.Collection; +using Kavita.Models.DTOs.KavitaPlus.ExternalMetadata; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Metadata.Matching; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.Entities.Enums; using Microsoft.Extensions.Logging; -namespace API.Services.Plus; - -/// -/// All Http requests to K+ should be contained in this service, the service will not handle any errors. -/// This is expected from the caller. -/// -public interface IKavitaPlusApiService -{ - Task HasTokenExpired(string license, string token, ScrobbleProvider provider); - Task GetRateLimit(string license, string token); - Task PostScrobbleUpdate(ScrobbleDto data, string license); - Task> GetMalStacks(string malUsername, string license); - Task> MatchSeries(MatchSeriesRequestDto request); - Task GetSeriesDetail(PlusSeriesRequestDto request); - Task GetSeriesDetailById(ExternalMetadataIdsDto request); -} +namespace Kavita.Services.Plus; public class KavitaPlusApiService(ILogger logger, IUnitOfWork unitOfWork): IKavitaPlusApiService { private const string ScrobblingPath = "/api/scrobbling/"; - public async Task HasTokenExpired(string license, string token, ScrobbleProvider provider) + public async Task HasTokenExpired(string license, string token, ScrobbleProvider provider, + CancellationToken ct = default) { var res = await Get(ScrobblingPath + "valid-key?provider=" + provider + "&key=" + token, license, token); var str = await res.GetStringAsync(); return bool.Parse(str); } - public async Task GetRateLimit(string license, string token) + public async Task GetRateLimit(string license, string token, CancellationToken ct = default) { var res = await Get(ScrobblingPath + "rate-limit?accessToken=" + token, license, token); var str = await res.GetStringAsync(); return int.Parse(str); } - public async Task PostScrobbleUpdate(ScrobbleDto data, string license) + public async Task PostScrobbleUpdate(ScrobbleDto data, string license, + CancellationToken ct = default) { return await PostAndReceive(ScrobblingPath + "update", data, license); } - public async Task> GetMalStacks(string malUsername, string license) + public async Task> GetMalStacks(string malUsername, string license, CancellationToken ct = default) { return await $"{Configuration.KavitaPlusApiUrl}/api/metadata/v2/stacks?username={malUsername}" .WithKavitaPlusHeaders(license) - .GetJsonAsync>(); + .GetJsonAsync>(cancellationToken: ct); } - public async Task> MatchSeries(MatchSeriesRequestDto request) + public async Task> MatchSeries(MatchSeriesRequestDto request, + CancellationToken ct = default) { - var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - var token = (await unitOfWork.UserRepository.GetDefaultAdminUser()).AniListAccessToken; + var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct)).Value; + var token = (await unitOfWork.UserRepository.GetDefaultAdminUser(ct: ct)).AniListAccessToken; return await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/match-series") .WithKavitaPlusHeaders(license, token) - .PostJsonAsync(request) + .PostJsonAsync(request, cancellationToken: ct) .ReceiveJson>(); } - public async Task GetSeriesDetail(PlusSeriesRequestDto request) + public async Task GetSeriesDetail(PlusSeriesRequestDto request, CancellationToken ct = default) { - var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - var token = (await unitOfWork.UserRepository.GetDefaultAdminUser()).AniListAccessToken; + var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct)).Value; + var token = (await unitOfWork.UserRepository.GetDefaultAdminUser(ct: ct)).AniListAccessToken; return await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-detail") .WithKavitaPlusHeaders(license, token) - .PostJsonAsync(request) + .PostJsonAsync(request, cancellationToken: ct) .ReceiveJson(); } - public async Task GetSeriesDetailById(ExternalMetadataIdsDto request) + public async Task GetSeriesDetailById(ExternalMetadataIdsDto request, + CancellationToken ct = default) { - var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - var token = (await unitOfWork.UserRepository.GetDefaultAdminUser()).AniListAccessToken; + var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct)).Value; + var token = (await unitOfWork.UserRepository.GetDefaultAdminUser(ct: ct)).AniListAccessToken; return await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-by-ids") .WithKavitaPlusHeaders(license, token) - .PostJsonAsync(request) + .PostJsonAsync(request, cancellationToken: ct) .ReceiveJson(); } diff --git a/API/Services/Plus/LicenseService.cs b/Kavita.Services/Plus/LicenseService.cs similarity index 84% rename from API/Services/Plus/LicenseService.cs rename to Kavita.Services/Plus/LicenseService.cs index c3d184f0a..c593c4d0b 100644 --- a/API/Services/Plus/LicenseService.cs +++ b/Kavita.Services/Plus/LicenseService.cs @@ -1,20 +1,21 @@ using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.Constants; -using API.Data; -using API.DTOs.KavitaPlus.License; -using API.Entities.Enums; -using API.Extensions; -using API.Services.Tasks; using EasyCaching.Core; using Flurl.Http; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Plus; using Kavita.Common; using Kavita.Common.EnvironmentInfo; +using Kavita.Common.Extensions; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.KavitaPlus.License; +using Kavita.Models.Entities.Enums; using Microsoft.Extensions.Logging; -namespace API.Services.Plus; -#nullable enable +namespace Kavita.Services.Plus; internal class RegisterLicenseResponseDto { @@ -23,18 +24,6 @@ internal class RegisterLicenseResponseDto public string ErrorMessage { get; set; } } -public interface ILicenseService -{ - //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); - Task GetLicenseInfo(bool forceCheck = false); - Task ResendWelcomeEmail(); -} - public class LicenseService( IEasyCachingProviderFactory cachingProviderFactory, IUnitOfWork unitOfWork, @@ -120,20 +109,21 @@ public class LicenseService( /// Checks licenses and updates cache /// /// Skip what's in cache + /// /// - public async Task HasActiveLicense(bool forceCheck = false) + public async Task HasActiveLicense(bool forceCheck = false, CancellationToken ct = default) { var provider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); if (!forceCheck) { - var cacheValue = await provider.GetAsync(CacheKey); + var cacheValue = await provider.GetAsync(CacheKey, ct); if (cacheValue.HasValue) return cacheValue.Value; } var result = false; try { - var serverSetting = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + var serverSetting = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct); result = await IsLicenseValid(serverSetting.Value); } catch (Exception ex) @@ -142,8 +132,8 @@ public class LicenseService( } finally { - await provider.FlushAsync(); - await provider.SetAsync(CacheKey, result, _licenseCacheTimeout); + await provider.FlushAsync(ct); + await provider.SetAsync(CacheKey, result, _licenseCacheTimeout, ct); } return result; @@ -153,8 +143,9 @@ public class LicenseService( /// Checks if the sub is active and caches the result. This should not be used too much over cache as it will skip backend caching. ///
/// + /// /// - public async Task HasActiveSubscription(string? license) + public async Task HasActiveSubscription(string? license, CancellationToken ct = default) { if (string.IsNullOrWhiteSpace(license)) return false; try @@ -165,14 +156,14 @@ public class LicenseService( { License = license, InstallId = HashUtil.ServerToken() - }) + }, cancellationToken: ct) .ReceiveString(); var result = bool.Parse(response); var provider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); - await provider.FlushAsync(); - await provider.SetAsync(CacheKey, result, _licenseCacheTimeout); + await provider.FlushAsync(ct); + await provider.SetAsync(CacheKey, result, _licenseCacheTimeout, ct); return result; } @@ -183,37 +174,37 @@ public class LicenseService( } } - public async Task RemoveLicense() + public async Task RemoveLicense(CancellationToken ct = default) { - var serverSetting = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + var serverSetting = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct); serverSetting.Value = string.Empty; unitOfWork.SettingsRepository.Update(serverSetting); - await unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); var provider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); - await provider.RemoveAsync(CacheKey); + await provider.RemoveAsync(CacheKey, ct); } - public async Task AddLicense(string license, string email, string? discordId) + public async Task AddLicense(string license, string email, string? discordId, CancellationToken ct = default) { - var serverSetting = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + var serverSetting = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct); 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(); + await unitOfWork.CommitAsync(ct); } - public async Task ResetLicense(string license, string email) + public async Task ResetLicense(string license, string email, CancellationToken ct = default) { try { - var encryptedLicense = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + var encryptedLicense = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct); var response = await (Configuration.KavitaPlusApiUrl + "/api/license/reset") .WithKavitaPlusHeaders(encryptedLicense.Value) .PostJsonAsync(new ResetLicenseDto() @@ -221,13 +212,13 @@ public class LicenseService( License = license.Trim(), InstallId = HashUtil.ServerToken(), EmailId = email - }) + }, cancellationToken: ct) .ReceiveString(); if (string.IsNullOrEmpty(response)) { var provider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); - await provider.RemoveAsync(CacheKey); + await provider.RemoveAsync(CacheKey, ct); return true; } @@ -246,12 +237,13 @@ public class LicenseService( /// Fetches information about the license from Kavita+. If there is no license or an exception, will return null and can be assumed it is not active ///
/// + /// /// - public async Task GetLicenseInfo(bool forceCheck = false) + public async Task GetLicenseInfo(bool forceCheck = false, CancellationToken ct = default) { // Check if there is a license var hasLicense = - !string.IsNullOrEmpty((await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)) + !string.IsNullOrEmpty((await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct)) .Value); if (!hasLicense) return null; @@ -260,22 +252,22 @@ public class LicenseService( var licenseInfoProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.LicenseInfo); if (!forceCheck) { - var cacheValue = await licenseInfoProvider.GetAsync(LicenseInfoCacheKey); + var cacheValue = await licenseInfoProvider.GetAsync(LicenseInfoCacheKey, ct); if (cacheValue.HasValue) return cacheValue.Value; } try { - var encryptedLicense = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + var encryptedLicense = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct); var response = await (Configuration.KavitaPlusApiUrl + "/api/license/info") .WithKavitaPlusHeaders(encryptedLicense.Value) - .GetJsonAsync(); + .GetJsonAsync(cancellationToken: ct); // This indicates a mismatch on installId or no active subscription if (response == null) return null; // Ensure that current version is within the 3 version limit. Don't count Nightly releases or Hotfixes - var releases = await versionUpdaterService.GetAllReleases(); + var releases = await versionUpdaterService.GetAllReleases(ct: ct); response.IsValidVersion = releases .Where(r => !r.UpdateTitle.Contains("Hotfix")) // We don't care about Hotfix releases .Where(r => !r.IsPrerelease) // Ensure we don't take current nightlies within the current/last stable @@ -287,9 +279,9 @@ public class LicenseService( // Cache if the license is valid here as well var licenseProvider = cachingProviderFactory.GetCachingProvider(EasyCacheProfiles.License); - await licenseProvider.SetAsync(CacheKey, response.IsActive, _licenseCacheTimeout); + await licenseProvider.SetAsync(CacheKey, response.IsActive, _licenseCacheTimeout, ct); - // TODO: If info.IsCancelled && notActive, let's remove the license so we aren't constantly checking + // default: If info.IsCancelled && notActive, let's remove the license so we aren't constantly checking if (response is {IsCancelled: true, IsActive: false}) { //logger.LogWarning("Kavita+ License is no longer active, removing Server registration"); @@ -298,7 +290,7 @@ public class LicenseService( // Cache the license info if IsActive and ExpirationDate > DateTime.UtcNow + 2 if (response.IsActive && response.ExpirationDate > DateTime.UtcNow.AddDays(2)) { - await licenseInfoProvider.SetAsync(LicenseInfoCacheKey, response, _licenseCacheTimeout); + await licenseInfoProvider.SetAsync(LicenseInfoCacheKey, response, _licenseCacheTimeout, ct); } @@ -315,17 +307,18 @@ public class LicenseService( /// /// Attempts to resend a welcome email to the registered user. The sub does not need to be active. /// + /// /// - public async Task ResendWelcomeEmail() + public async Task ResendWelcomeEmail(CancellationToken ct = default) { try { - var encryptedLicense = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey); + var encryptedLicense = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct); if (string.IsNullOrEmpty(encryptedLicense.Value)) return false; var httpResponse = await (Configuration.KavitaPlusApiUrl + "/api/license/resend-welcome-email") .WithKavitaPlusHeaders(encryptedLicense.Value) - .PostAsync(); + .PostAsync(cancellationToken: ct); var response = await httpResponse.GetStringAsync(); diff --git a/API/Services/Plus/ScrobblingService.cs b/Kavita.Services/Plus/ScrobblingService.cs similarity index 82% rename from API/Services/Plus/ScrobblingService.cs rename to Kavita.Services/Plus/ScrobblingService.cs index 00c7a9c84..5ff3581e7 100644 --- a/API/Services/Plus/ScrobblingService.cs +++ b/Kavita.Services/Plus/ScrobblingService.cs @@ -1,121 +1,36 @@ -using System; + + +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.Filtering; -using API.DTOs.Scrobbling; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Entities.Scrobble; -using API.Extensions; -using API.Helpers; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; using Flurl.Http; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Services.SignalR; using Kavita.Common; using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.Scrobbling; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.Scrobble; +using Kavita.Models.Entities.User; +using Kavita.Models.Extensions; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Services.Plus; -#nullable enable - -/// -/// Misleading name but is the source of data (like a review coming from AniList) -/// -public enum ScrobbleProvider -{ - /// - /// For now, this means data comes from within this instance of Kavita - /// - Kavita = 0, - AniList = 1, - Mal = 2, - [Obsolete("No longer supported")] - GoogleBooks = 3, - Cbr = 4 -} - -public interface IScrobblingService -{ - /// - /// An automated job that will run against all user's tokens and validate if they are still active - /// - /// This service can validate without license check as the task which calls will be guarded - /// - Task CheckExternalAccessTokens(); - - /// - /// Checks if the token has expired with , if it has double checks with K+, - /// otherwise return false. - /// - /// - /// - /// - /// Returns true if there is no license present - Task HasTokenExpired(int userId, ScrobbleProvider provider); - /// - /// Create, or update a non-processed, event, for the given series - /// - /// - /// - /// - /// - Task ScrobbleRatingUpdate(int userId, int seriesId, float rating); - /// - /// NOP, until hardcover support has been worked out - /// - /// - /// - /// - /// - /// - Task ScrobbleReviewUpdate(int userId, int seriesId, string? reviewTitle, string reviewBody); - /// - /// Create, or update a non-processed, event, for the given series - /// - /// - /// - /// - Task ScrobbleReadingUpdate(int userId, int seriesId); - /// - /// Creates an or for - /// the given series - /// - /// - /// - /// - /// - /// Only the result of both WantToRead types is send to K+ - Task ScrobbleWantToReadUpdate(int userId, int seriesId, bool onWantToRead); - - /// - /// Removed all processed events that are at least 7 days old - /// - /// - [DisableConcurrentExecution(60 * 60 * 60)] - [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - public Task ClearProcessedEvents(); - - /// - /// Makes K+ requests for all non-processed events until rate limits are reached - /// - /// - [DisableConcurrentExecution(60 * 60 * 60)] - [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - Task ProcessUpdatesSinceLastSync(); - - Task CreateEventsFromExistingHistory(int userId = 0); - Task CreateEventsFromExistingHistoryForSeries(int seriesId); - Task ClearEventsForSeries(int userId, int seriesId); -} +namespace Kavita.Services.Plus; /// /// Context used when syncing scrobble events. Do NOT reuse between syncs @@ -166,26 +81,15 @@ public class ScrobblingService : IScrobblingService private readonly IEmailService _emailService; private readonly IKavitaPlusApiService _kavitaPlusApiService; - public const string AniListWeblinkWebsite = "https://anilist.co/manga/"; - public const string MalWeblinkWebsite = "https://myanimelist.net/manga/"; - public const string MalStaffWebsite = "https://myanimelist.net/people/"; - public const string MalCharacterWebsite = "https://myanimelist.net/character/"; - public const string GoogleBooksWeblinkWebsite = "https://books.google.com/books?id="; - public const string MangaDexWeblinkWebsite = "https://mangadex.org/title/"; - public const string AniListStaffWebsite = "https://anilist.co/staff/"; - public const string AniListCharacterWebsite = "https://anilist.co/character/"; - public const string HardcoverStaffWebsite = "https://hardcover.app/authors/"; - - - private static readonly Dictionary WeblinkExtractionMap = new() - { - {AniListWeblinkWebsite, 0}, - {MalWeblinkWebsite, 0}, - {GoogleBooksWeblinkWebsite, 0}, - {MangaDexWeblinkWebsite, 0}, - {AniListStaffWebsite, 0}, - {AniListCharacterWebsite, 0}, - }; + public const string AniListWeblinkWebsite = ScrobblingHelper.AniListWeblinkWebsite; + public const string MalWeblinkWebsite = ScrobblingHelper.MalWeblinkWebsite; + public const string MalStaffWebsite = ScrobblingHelper.MalStaffWebsite; + public const string MalCharacterWebsite = ScrobblingHelper.MalCharacterWebsite; + public const string GoogleBooksWeblinkWebsite = ScrobblingHelper.GoogleBooksWeblinkWebsite; + public const string MangaDexWeblinkWebsite = ScrobblingHelper.MangaDexWeblinkWebsite; + public const string AniListStaffWebsite = ScrobblingHelper.AniListStaffWebsite; + public const string AniListCharacterWebsite = ScrobblingHelper.AniListCharacterWebsite; + public const string HardcoverStaffWebsite = ScrobblingHelper.HardcoverStaffWebsite; private const int ScrobbleSleepTime = 1000; // We can likely tie this to AniList's 90 rate / min ((60 * 1000) / 90) @@ -226,12 +130,13 @@ public class ScrobblingService : IScrobblingService /// /// An automated job that will run against all user's tokens and validate if they are still active /// + /// /// This service can validate without license check as the task which calls will be guarded /// - public async Task CheckExternalAccessTokens() + public async Task CheckExternalAccessTokens(CancellationToken ct = default) { // Validate AniList - var users = await _unitOfWork.UserRepository.GetAllUsersAsync(); + var users = await _unitOfWork.UserRepository.GetAllUsersAsync(ct: ct); foreach (var user in users) { if (string.IsNullOrEmpty(user.AniListAccessToken)) continue; @@ -261,7 +166,7 @@ public class ScrobblingService : IScrobblingService await _eventHub.SendMessageToAsync( MessageFactory.ScrobblingKeyExpired, MessageFactory.ScrobblingKeyExpiredEvent(ScrobbleProvider.AniList), - user.Id); + user.Id, ct); } } @@ -298,7 +203,7 @@ public class ScrobblingService : IScrobblingService return !hasAlreadySentExpirationEmail; } - public async Task HasTokenExpired(int userId, ScrobbleProvider provider) + public async Task HasTokenExpired(int userId, ScrobbleProvider provider, CancellationToken ct = default) { var token = await GetTokenForProvider(userId, provider); @@ -306,7 +211,7 @@ public class ScrobblingService : IScrobblingService { // NOTE: Should this side effect be here? await _eventHub.SendMessageToAsync(MessageFactory.ScrobblingKeyExpired, - MessageFactory.ScrobblingKeyExpiredEvent(ScrobbleProvider.AniList), userId); + MessageFactory.ScrobblingKeyExpiredEvent(ScrobbleProvider.AniList), userId, ct); return true; } @@ -352,27 +257,28 @@ public class ScrobblingService : IScrobblingService #region Scrobble ingest - public Task ScrobbleReviewUpdate(int userId, int seriesId, string? reviewTitle, string reviewBody) + public Task ScrobbleReviewUpdate(int userId, int seriesId, string? reviewTitle, string reviewBody, + CancellationToken ct = default) { // Currently disabled until at least hardcover is implemented return Task.CompletedTask; } - public async Task ScrobbleRatingUpdate(int userId, int seriesId, float rating) + public async Task ScrobbleRatingUpdate(int userId, int seriesId, float rating, CancellationToken ct = default) { - if (!await _licenseService.HasActiveLicense()) return; + if (!await _licenseService.HasActiveLicense(ct: ct)) return; - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.ExternalMetadata); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.ExternalMetadata, ct); if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist")); - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences, ct); if (user == null || !user.UserPreferences.AniListScrobblingEnabled) return; _logger.LogInformation("Processing Scrobbling rating event for {AppUserId} on {SeriesName}", userId, series.Name); if (await CheckIfCannotScrobble(userId, seriesId, series)) return; var existingEvt = await _unitOfWork.ScrobbleRepository.GetEvent(userId, series.Id, - ScrobbleEventType.ScoreUpdated, true); + ScrobbleEventType.ScoreUpdated, true, ct); if (existingEvt is {IsProcessed: false}) { // We need to just update Volume/Chapter number @@ -380,7 +286,7 @@ public class ScrobblingService : IScrobblingService existingEvt.Series.Name, existingEvt.Rating, rating); existingEvt.Rating = rating; _unitOfWork.ScrobbleRepository.Update(existingEvt); - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); return; } @@ -389,45 +295,45 @@ public class ScrobblingService : IScrobblingService SeriesId = series.Id, LibraryId = series.LibraryId, ScrobbleEventType = ScrobbleEventType.ScoreUpdated, - AniListId = GetAniListId(series), - MalId = GetMalId(series), + AniListId = ScrobblingHelper.GetAniListId(series), + MalId = ScrobblingHelper.GetMalId(series), AppUserId = userId, Format = series.Library.Type.ConvertToPlusMediaFormat(series.Format), Rating = rating }; _unitOfWork.ScrobbleRepository.Attach(evt); - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); _logger.LogDebug("Added Scrobbling Rating update on {SeriesName} with Userid {AppUserId}", series.Name, userId); } - public async Task ScrobbleReadingUpdate(int userId, int seriesId) + public async Task ScrobbleReadingUpdate(int userId, int seriesId, CancellationToken ct = default) { - if (!await _licenseService.HasActiveLicense()) return; + if (!await _licenseService.HasActiveLicense(ct: ct)) return; - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.ExternalMetadata); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.ExternalMetadata, ct); if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist")); - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences, ct); if (user == null || !user.UserPreferences.AniListScrobblingEnabled) return; _logger.LogInformation("Processing Scrobbling reading event for {AppUserId} on {SeriesName}", userId, series.Name); if (await CheckIfCannotScrobble(userId, seriesId, series)) return; - var isAnyProgressOnSeries = await _unitOfWork.AppUserProgressRepository.HasAnyProgressOnSeriesAsync(seriesId, userId); + var isAnyProgressOnSeries = await _unitOfWork.AppUserProgressRepository.HasAnyProgressOnSeriesAsync(seriesId, userId, ct); - var volumeNumber = (int) await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId); - var chapterNumber = await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(seriesId, userId); + var volumeNumber = (int) await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId, ct); + var chapterNumber = await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(seriesId, userId, ct); // Check if there is an existing not yet processed event, if so update it var existingEvt = await _unitOfWork.ScrobbleRepository.GetEvent(userId, series.Id, - ScrobbleEventType.ChapterRead, true); + ScrobbleEventType.ChapterRead, true, ct); if (existingEvt is {IsProcessed: false}) { if (!isAnyProgressOnSeries) { _unitOfWork.ScrobbleRepository.Remove(existingEvt); - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); _logger.LogDebug("Removed scrobble event for {Series} as there is no reading progress", series.Name); return; } @@ -440,7 +346,7 @@ public class ScrobblingService : IScrobblingService existingEvt.ChapterNumber = chapterNumber; _unitOfWork.ScrobbleRepository.Update(existingEvt); - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); _logger.LogDebug("Overriding scrobble event for {Series} from vol {PrevVol} ch {PrevChap} -> vol {UpdatedVol} ch {UpdatedChap}", existingEvt.Series.Name, prevVol, prevChapter, existingEvt.VolumeNumber, existingEvt.ChapterNumber); @@ -460,8 +366,8 @@ public class ScrobblingService : IScrobblingService SeriesId = series.Id, LibraryId = series.LibraryId, ScrobbleEventType = ScrobbleEventType.ChapterRead, - AniListId = GetAniListId(series), - MalId = GetMalId(series), + AniListId = ScrobblingHelper.GetAniListId(series), + MalId = ScrobblingHelper.GetMalId(series), AppUserId = userId, VolumeNumber = volumeNumber, ChapterNumber = chapterNumber, @@ -475,7 +381,7 @@ public class ScrobblingService : IScrobblingService } _unitOfWork.ScrobbleRepository.Attach(evt); - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); _logger.LogDebug("Added Scrobbling Read update on {SeriesName} - Volume: {VolumeNumber} Chapter: {ChapterNumber} for User: {AppUserId}", series.Name, evt.VolumeNumber, evt.ChapterNumber, userId); } catch (Exception ex) @@ -484,23 +390,23 @@ public class ScrobblingService : IScrobblingService } } - public async Task ScrobbleWantToReadUpdate(int userId, int seriesId, bool onWantToRead) + public async Task ScrobbleWantToReadUpdate(int userId, int seriesId, bool onWantToRead, CancellationToken ct = default) { - if (!await _licenseService.HasActiveLicense()) return; + if (!await _licenseService.HasActiveLicense(ct: ct)) return; - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.ExternalMetadata); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library | SeriesIncludes.ExternalMetadata, ct); if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist")); if (!series.Library.AllowScrobbling) return; - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences); + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences, ct); if (user == null || !user.UserPreferences.AniListScrobblingEnabled) return; if (await CheckIfCannotScrobble(userId, seriesId, series)) return; _logger.LogInformation("Processing Scrobbling want-to-read event for {AppUserId} on {SeriesName}", userId, series.Name); // Get existing events for this series/user - var existingEvents = (await _unitOfWork.ScrobbleRepository.GetUserEventsForSeries(userId, seriesId)) + var existingEvents = (await _unitOfWork.ScrobbleRepository.GetUserEventsForSeries(userId, seriesId, ct)) .Where(e => new[] { ScrobbleEventType.AddWantToRead, ScrobbleEventType.RemoveWantToRead }.Contains(e.ScrobbleEventType)); // Remove all existing want-to-read events for this series/user @@ -512,114 +418,19 @@ public class ScrobblingService : IScrobblingService SeriesId = series.Id, LibraryId = series.LibraryId, ScrobbleEventType = onWantToRead ? ScrobbleEventType.AddWantToRead : ScrobbleEventType.RemoveWantToRead, - AniListId = GetAniListId(series), - MalId = GetMalId(series), + AniListId = ScrobblingHelper.GetAniListId(series), + MalId = ScrobblingHelper.GetMalId(series), AppUserId = userId, Format = series.Library.Type.ConvertToPlusMediaFormat(series.Format), }; _unitOfWork.ScrobbleRepository.Attach(evt); - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); _logger.LogDebug("Added Scrobbling WantToRead update on {SeriesName} with Userid {AppUserId} ", series.Name, userId); } #endregion - #region Scrobble provider methods - - private static bool IsAniListReviewValid(string reviewTitle, string reviewBody) - { - return string.IsNullOrEmpty(reviewTitle) || string.IsNullOrEmpty(reviewBody) || (reviewTitle.Length < 2200 || - reviewTitle.Length > 120 || - reviewTitle.Length < 20); - } - - public static long? GetMalId(Series series) - { - var malId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite); - return malId ?? series.ExternalSeriesMetadata?.MalId; - } - - public static int? GetAniListId(Series seriesWithExternalMetadata) - { - var aniListId = ExtractId(seriesWithExternalMetadata.Metadata.WebLinks, AniListWeblinkWebsite); - return aniListId ?? seriesWithExternalMetadata.ExternalSeriesMetadata?.AniListId; - } - - /// - /// Extract an Id from a given weblink - /// - /// - /// - /// - public static T? ExtractId(string webLinks, string website) - { - var index = WeblinkExtractionMap[website]; - foreach (var webLink in webLinks.Split(',')) - { - if (!webLink.StartsWith(website)) continue; - - var tokens = webLink.Split(website)[1].Split('/'); - var value = tokens[index]; - - if (typeof(T) == typeof(int?)) - { - if (int.TryParse(value, CultureInfo.InvariantCulture, out var intValue)) return (T)(object)intValue; - } - else if (typeof(T) == typeof(int)) - { - if (int.TryParse(value, CultureInfo.InvariantCulture, out var intValue)) return (T)(object)intValue; - - return default; - } - else if (typeof(T) == typeof(long?)) - { - if (long.TryParse(value, CultureInfo.InvariantCulture, out var longValue)) return (T)(object)longValue; - } - else if (typeof(T) == typeof(string)) - { - return (T)(object)value; - } - } - - return default; - } - - /// - /// Generate a URL from a given ID and website - /// - /// Type of the ID (e.g., int, long, string) - /// The ID to embed in the URL - /// The base website URL - /// The generated URL or null if the website is not supported - public static string? GenerateUrl(T id, string website) - { - if (!WeblinkExtractionMap.ContainsKey(website)) - { - return null; // Unsupported website - } - - if (Equals(id, default(T))) - { - throw new ArgumentNullException(nameof(id), "ID cannot be null."); - } - - // Ensure the type of the ID matches supported types - if (typeof(T) == typeof(int) || typeof(T) == typeof(long) || typeof(T) == typeof(string)) - { - return $"{website}{id}"; - } - - throw new ArgumentException("Unsupported ID type. Supported types are int, long, and string.", nameof(id)); - } - - public static string CreateUrl(string url, long? id) - { - return id is null or 0 ? string.Empty : $"{url}{id}/"; - } - - #endregion - /// /// Returns false if, the series is on hold or Don't Match, or when the library has scrobbling disable or not eligible /// @@ -761,9 +572,10 @@ public class ScrobblingService : IScrobblingService /// This is a task that is run on a fixed schedule (every few hours or every day) that clears out the scrobble event table /// and offloads the data to the API server which performs the syncing to the providers. /// + /// [DisableConcurrentExecution(60 * 60 * 60)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - public async Task ProcessUpdatesSinceLastSync() + public async Task ProcessUpdatesSinceLastSync(CancellationToken ct = default) { var ctx = await PrepareScrobbleContext(); if (ctx.TotalCount == 0) return; @@ -1210,18 +1022,18 @@ public class ScrobblingService : IScrobblingService #region BackFill - /// /// This will backfill events from existing progress history, ratings, and want to read for users that have a valid license /// /// Defaults to 0 meaning all users. Allows a userId to be set if a scrobble key is added to a user - public async Task CreateEventsFromExistingHistory(int userId = 0) + /// + public async Task CreateEventsFromExistingHistory(int userId = 0, CancellationToken ct = default) { - if (!await _licenseService.HasActiveLicense()) return; + if (!await _licenseService.HasActiveLicense(ct: ct)) return; if (userId != 0) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, ct: ct); if (user == null || string.IsNullOrEmpty(user.AniListAccessToken)) return; if (user.HasRunScrobbleEventGeneration) { @@ -1230,10 +1042,10 @@ public class ScrobblingService : IScrobblingService } } - var libAllowsScrobbling = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()) + var libAllowsScrobbling = (await _unitOfWork.LibraryRepository.GetLibrariesAsync(ct: ct)) .ToDictionary(lib => lib.Id, lib => lib.AllowScrobbling); - var userIds = (await _unitOfWork.UserRepository.GetAllUsersAsync()) + var userIds = (await _unitOfWork.UserRepository.GetAllUsersAsync(ct: ct)) .Where(l => userId == 0 || userId == l.Id) .Where(u => !u.HasRunScrobbleEventGeneration) .Select(u => u.Id); @@ -1299,42 +1111,42 @@ public class ScrobblingService : IScrobblingService } } - public async Task CreateEventsFromExistingHistoryForSeries(int seriesId) + public async Task CreateEventsFromExistingHistoryForSeries(int seriesId, CancellationToken ct = default) { - if (!await _licenseService.HasActiveLicense()) return; + if (!await _licenseService.HasActiveLicense(ct: ct)) return; - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library, ct); if (series == null || !series.Library.AllowScrobbling) return; _logger.LogInformation("Creating Scrobbling events for Series {SeriesName}", series.Name); - var userIds = (await _unitOfWork.UserRepository.GetAllUsersAsync()).Select(u => u.Id); + var userIds = (await _unitOfWork.UserRepository.GetAllUsersAsync(ct: ct)).Select(u => u.Id); foreach (var uId in userIds) { // Handle "Want to Read" updates specific to the series - var wantToRead = await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(uId); + var wantToRead = await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(uId, ct); foreach (var wtr in wantToRead.Where(wtr => wtr.Id == seriesId)) { - await ScrobbleWantToReadUpdate(uId, wtr.Id, true); + await ScrobbleWantToReadUpdate(uId, wtr.Id, true, ct); } // Handle ratings specific to the series - var ratings = await _unitOfWork.UserRepository.GetSeriesWithRatings(uId); + var ratings = await _unitOfWork.UserRepository.GetSeriesWithRatings(uId, ct); foreach (var rating in ratings.Where(rating => rating.SeriesId == seriesId)) { - await ScrobbleRatingUpdate(uId, rating.SeriesId, rating.Rating); + await ScrobbleRatingUpdate(uId, rating.SeriesId, rating.Rating, ct); } // Handle review specific to the series - var reviews = await _unitOfWork.UserRepository.GetSeriesWithReviews(uId); + var reviews = await _unitOfWork.UserRepository.GetSeriesWithReviews(uId, ct); foreach (var review in reviews.Where(r => r.SeriesId == seriesId && !string.IsNullOrEmpty(r.Review))) { - await ScrobbleReviewUpdate(uId, review.SeriesId, string.Empty, review.Review!); + await ScrobbleReviewUpdate(uId, review.SeriesId, string.Empty, review.Review!, ct); } // Handle progress updates for the specific series - await ScrobbleReadingUpdate(uId, seriesId); + await ScrobbleReadingUpdate(uId, seriesId, ct); } } @@ -1345,27 +1157,29 @@ public class ScrobblingService : IScrobblingService ///
/// /// - public async Task ClearEventsForSeries(int userId, int seriesId) + /// + public async Task ClearEventsForSeries(int userId, int seriesId, CancellationToken ct = default) { _logger.LogInformation("Clearing Pre-existing Scrobble events for Series {SeriesId} by User {AppUserId} as Series is now on hold list", seriesId, userId); - var events = await _unitOfWork.ScrobbleRepository.GetUserEventsForSeries(userId, seriesId); + var events = await _unitOfWork.ScrobbleRepository.GetUserEventsForSeries(userId, seriesId, ct); _unitOfWork.ScrobbleRepository.Remove(events); - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); } /// /// 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() + public async Task ClearProcessedEvents(CancellationToken ct = default) { const int daysAgo = 7; - var events = await _unitOfWork.ScrobbleRepository.GetProcessedEvents(daysAgo); + var events = await _unitOfWork.ScrobbleRepository.GetProcessedEvents(daysAgo, ct); _unitOfWork.ScrobbleRepository.Remove(events); _logger.LogInformation("Removing {Count} scrobble events that have been processed {DaysAgo}+ days ago", events.Count, daysAgo); - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); } private static bool CanProcessScrobbleEvent(ScrobbleEvent readEvent) diff --git a/API/Services/Plus/SmartCollectionSyncService.cs b/Kavita.Services/Plus/SmartCollectionSyncService.cs similarity index 61% rename from API/Services/Plus/SmartCollectionSyncService.cs rename to Kavita.Services/Plus/SmartCollectionSyncService.cs index c56054d3d..df5e70f8b 100644 --- a/API/Services/Plus/SmartCollectionSyncService.cs +++ b/Kavita.Services/Plus/SmartCollectionSyncService.cs @@ -3,21 +3,25 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.KavitaPlus.ExternalMetadata; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers; -using API.SignalR; using Flurl.Http; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services.Plus; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.KavitaPlus.ExternalMetadata; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Models.Extensions; using Microsoft.Extensions.Logging; -namespace API.Services.Plus; -#nullable enable +namespace Kavita.Services.Plus; internal sealed class SeriesCollection { @@ -30,63 +34,38 @@ internal sealed class SeriesCollection public int TotalItems { get; set; } } -/// -/// Responsible to synchronize Collection series from non-Kavita sources -/// -public interface ISmartCollectionSyncService +public class SmartCollectionSyncService( + IUnitOfWork unitOfWork, + ILogger logger, + IEventHub eventHub, + ILicenseService licenseService) + : ISmartCollectionSyncService { - /// - /// Synchronize all collections - /// - /// - Task Sync(); - /// - /// Synchronize a collection - /// - /// - /// - Task Sync(int collectionId); -} - -public class SmartCollectionSyncService : ISmartCollectionSyncService -{ - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - private readonly IEventHub _eventHub; - private readonly ILicenseService _licenseService; - private const int SyncDelta = -2; // Allow 50 requests per 24 hours private static readonly RateLimiter RateLimiter = new RateLimiter(50, TimeSpan.FromHours(24), false); - public SmartCollectionSyncService(IUnitOfWork unitOfWork, ILogger logger, - IEventHub eventHub, ILicenseService licenseService) - { - _unitOfWork = unitOfWork; - _logger = logger; - _eventHub = eventHub; - _licenseService = licenseService; - } - /// /// For every Sync-eligible collection, synchronize with upstream /// + /// /// - public async Task Sync() + public async Task Sync(CancellationToken ct = default) { - if (!await _licenseService.HasActiveLicense()) return; + if (!await licenseService.HasActiveLicense(ct: ct)) return; + var expirationTime = DateTime.UtcNow.AddDays(SyncDelta).Truncate(TimeSpan.TicksPerHour); - var collections = (await _unitOfWork.CollectionTagRepository.GetAllCollectionsForSyncing(expirationTime)) + var collections = (await unitOfWork.CollectionTagRepository.GetAllCollectionsForSyncing(expirationTime, ct)) .Where(CanSync) .ToList(); - _logger.LogInformation("Found {Count} collections to synchronize", collections.Count); + logger.LogInformation("Found {Count} collections to synchronize", collections.Count); foreach (var collection in collections) { try { - await SyncCollection(collection); + await SyncCollection(collection, ct); } catch (RateLimitException) { @@ -94,22 +73,23 @@ public class SmartCollectionSyncService : ISmartCollectionSyncService } } - _logger.LogInformation("Synchronization complete"); + logger.LogInformation("Synchronization complete"); } - public async Task Sync(int collectionId) + public async Task Sync(int collectionId, CancellationToken ct = default) { - if (!await _licenseService.HasActiveLicense()) return; - var collection = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(collectionId, CollectionIncludes.Series); + if (!await licenseService.HasActiveLicense(ct: ct)) return; + + var collection = await unitOfWork.CollectionTagRepository.GetCollectionAsync(collectionId, CollectionIncludes.Series, ct); if (!CanSync(collection)) { - _logger.LogInformation("Requested to sync {CollectionName} but not applicable to sync", collection!.Title); + logger.LogInformation("Requested to sync {CollectionName} but not applicable to sync", collection!.Title); return; } try { - await SyncCollection(collection!); + await SyncCollection(collection!, ct); } catch (RateLimitException) {/* Swallow */} } @@ -121,28 +101,28 @@ public class SmartCollectionSyncService : ISmartCollectionSyncService return true; } - private async Task SyncCollection(AppUserCollection collection) + private async Task SyncCollection(AppUserCollection collection, CancellationToken ct = default) { if (!RateLimiter.TryAcquire(string.Empty)) { // Request not allowed due to rate limit - _logger.LogDebug("Rate Limit hit for Smart Collection Sync"); + logger.LogDebug("Rate Limit hit for Smart Collection Sync"); throw new RateLimitException(); } var info = await GetStackInfo(GetStackId(collection.SourceUrl!)); if (info == null) { - _logger.LogInformation("Unable to find collection through Kavita+"); + logger.LogInformation("Unable to find collection through Kavita+"); return; } // Check each series in the collection against what's in the target // For everything that's not there, link it up for this user. - _logger.LogInformation("Starting Sync on {CollectionName} with {SeriesCount} Series", info.Title, info.TotalItems); + logger.LogInformation("Starting Sync on {CollectionName} with {SeriesCount} Series", info.Title, info.TotalItems); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.SmartCollectionProgressEvent(info.Title, string.Empty, 0, info.TotalItems, ProgressEventType.Started)); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.SmartCollectionProgressEvent(info.Title, string.Empty, 0, info.TotalItems, ProgressEventType.Started), ct: ct); var missingCount = 0; var missingSeries = new StringBuilder(); @@ -168,20 +148,20 @@ public class SmartCollectionSyncService : ISmartCollectionSyncService s.NormalizedLocalizedName == normalizedSeriesName) && formats.Contains(s.Format)); - _logger.LogDebug("Trying to find {SeriesName} with formats ({Formats}) within Kavita for linking. Found: {ExistingSeriesName} ({ExistingSeriesId})", + logger.LogDebug("Trying to find {SeriesName} with formats ({Formats}) within Kavita for linking. Found: {ExistingSeriesName} ({ExistingSeriesId})", seriesInfo.SeriesName, formats, existingSeries?.Name, existingSeries?.Id); if (existingSeries != null) { - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.SmartCollectionProgressEvent(info.Title, seriesInfo.SeriesName, counter, info.TotalItems, ProgressEventType.Updated)); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.SmartCollectionProgressEvent(info.Title, seriesInfo.SeriesName, counter, info.TotalItems, ProgressEventType.Updated), ct: ct); continue; } // Series not found in the collection, try to find it in the server - var newSeries = await _unitOfWork.SeriesRepository.GetSeriesByAnyName(seriesInfo.SeriesName, + var newSeries = await unitOfWork.SeriesRepository.GetSeriesByAnyName(seriesInfo.SeriesName, seriesInfo.LocalizedSeriesName, - formats, collection.AppUserId); + formats, collection.AppUserId, ct: ct); collection.Items ??= new List(); if (newSeries != null) @@ -192,7 +172,7 @@ public class SmartCollectionSyncService : ISmartCollectionSyncService } else { - _logger.LogDebug("{Series} not found in the server", seriesInfo.SeriesName); + logger.LogDebug("{Series} not found in the server", seriesInfo.SeriesName); missingCount++; missingSeries.Append( $"{seriesInfo.SeriesName}"); @@ -201,15 +181,15 @@ public class SmartCollectionSyncService : ISmartCollectionSyncService } catch (Exception ex) { - _logger.LogError(ex, "An exception occured when linking up a series to the collection. Skipping"); + logger.LogError(ex, "An exception occured when linking up a series to the collection. Skipping"); missingCount++; missingSeries.Append( $"{seriesInfo.SeriesName}"); missingSeries.Append("
"); } - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.SmartCollectionProgressEvent(info.Title, seriesInfo.SeriesName, counter, info.TotalItems, ProgressEventType.Updated)); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.SmartCollectionProgressEvent(info.Title, seriesInfo.SeriesName, counter, info.TotalItems, ProgressEventType.Updated), ct: ct); } // At this point, all series in the info have been checked and added if necessary @@ -218,26 +198,26 @@ public class SmartCollectionSyncService : ISmartCollectionSyncService collection.Summary = info.Summary; collection.MissingSeriesFromSource = missingSeries.ToString(); - _unitOfWork.CollectionTagRepository.Update(collection); + unitOfWork.CollectionTagRepository.Update(collection); try { - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); - await _unitOfWork.CollectionTagRepository.UpdateCollectionAgeRating(collection); + await unitOfWork.CollectionTagRepository.UpdateCollectionAgeRating(collection, ct); - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.SmartCollectionProgressEvent(info.Title, string.Empty, info.TotalItems, info.TotalItems, ProgressEventType.Ended)); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.SmartCollectionProgressEvent(info.Title, string.Empty, info.TotalItems, info.TotalItems, ProgressEventType.Ended), ct: ct); - await _eventHub.SendMessageAsync(MessageFactory.CollectionUpdated, - MessageFactory.CollectionUpdatedEvent(collection.Id), false); + await eventHub.SendMessageAsync(MessageFactory.CollectionUpdated, + MessageFactory.CollectionUpdatedEvent(collection.Id), false, ct); - _logger.LogInformation("Finished Syncing Collection {CollectionName} - Missing {MissingCount} series", + logger.LogInformation("Finished Syncing Collection {CollectionName} - Missing {MissingCount} series", collection.Title, missingCount); } catch (Exception ex) { - _logger.LogError(ex, "There was an error during saving the collection"); + logger.LogError(ex, "There was an error during saving the collection"); } } @@ -251,9 +231,9 @@ public class SmartCollectionSyncService : ISmartCollectionSyncService private async Task GetStackInfo(long stackId) { - _logger.LogDebug("Fetching Kavita+ for MAL Stack"); + logger.LogDebug("Fetching Kavita+ for MAL Stack"); - var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; + var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; var seriesForStack = await ($"{Configuration.KavitaPlusApiUrl}/api/metadata/v2/stack?stackId=" + stackId) .WithKavitaPlusHeaders(license) diff --git a/API/Services/Plus/WantToReadSyncService.cs b/Kavita.Services/Plus/WantToReadSyncService.cs similarity index 62% rename from API/Services/Plus/WantToReadSyncService.cs rename to Kavita.Services/Plus/WantToReadSyncService.cs index 7eefa44d4..26cd595af 100644 --- a/API/Services/Plus/WantToReadSyncService.cs +++ b/Kavita.Services/Plus/WantToReadSyncService.cs @@ -1,68 +1,58 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.KavitaPlus.Metadata; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; using Flurl.Http; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services.Plus; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; using Microsoft.Extensions.Logging; -namespace API.Services.Plus; +namespace Kavita.Services.Plus; -public interface IWantToReadSyncService -{ - Task Sync(); -} - /// /// Responsible for syncing Want To Read from upstream providers with Kavita /// -public class WantToReadSyncService : IWantToReadSyncService +public class WantToReadSyncService( + IUnitOfWork unitOfWork, + ILogger logger, + ILicenseService licenseService) + : IWantToReadSyncService { - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - private readonly ILicenseService _licenseService; - - public WantToReadSyncService(IUnitOfWork unitOfWork, ILogger logger, ILicenseService licenseService) + public async Task Sync(CancellationToken ct = default) { - _unitOfWork = unitOfWork; - _logger = logger; - _licenseService = licenseService; - } + if (!await licenseService.HasActiveLicense(ct: ct)) return; - public async Task Sync() - { - if (!await _licenseService.HasActiveLicense()) return; + var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, ct)).Value; - var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - - var users = await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.WantToRead | AppUserIncludes.UserPreferences); + var users = await unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.WantToRead | AppUserIncludes.UserPreferences, ct: ct); foreach (var user in users.Where(u => u.UserPreferences.WantToReadSync)) { if (string.IsNullOrEmpty(user.MalUserName) && string.IsNullOrEmpty(user.AniListAccessToken)) continue; try { - _logger.LogInformation("Syncing want to read for user: {UserName}", user.UserName); + logger.LogInformation("Syncing want to read for user: {UserName}", user.UserName); var wantToReadSeries = await ( $"{Configuration.KavitaPlusApiUrl}/api/metadata/v2/want-to-read?malUsername={user.MalUserName}&aniListToken={user.AniListAccessToken}") .WithKavitaPlusHeaders(license) .WithTimeout( TimeSpan.FromSeconds(120)) // Give extra time as MAL + AniList can result in a lot of data - .GetJsonAsync>(); + .GetJsonAsync>(cancellationToken: ct); // Match the series (note: There may be duplicates in the final result) foreach (var unmatchedSeries in wantToReadSeries) { - var match = await _unitOfWork.SeriesRepository.MatchSeries(unmatchedSeries); + var match = await unitOfWork.SeriesRepository.MatchSeries(unmatchedSeries, ct); if (match == null) { continue; @@ -73,7 +63,7 @@ public class WantToReadSyncService : IWantToReadSyncService { SeriesId = match.Id, }); - _logger.LogDebug("Added {MatchName} ({Format}) to Want to Read", match.Name, match.Format); + logger.LogDebug("Added {MatchName} ({Format}) to Want to Read", match.Name, match.Format); } // Remove existing Want to Read that are duplicates @@ -82,15 +72,15 @@ public class WantToReadSyncService : IWantToReadSyncService // TODO: Need to write in the history table the last sync time // Save the left over entities - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(ct); // Trigger CleanupService to cleanup any series in WantToRead that don't belong RecurringJob.TriggerJob(TaskScheduler.RemoveFromWantToReadTaskId); } catch (Exception ex) { - _logger.LogError(ex, "There was an exception when processing want to read series sync for {User}", user.UserName); + logger.LogError(ex, "There was an exception when processing want to read series sync for {User}", user.UserName); } } diff --git a/Kavita.Services/RatingService.cs b/Kavita.Services/RatingService.cs new file mode 100644 index 000000000..31e167632 --- /dev/null +++ b/Kavita.Services/RatingService.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.Models.DTOs; +using Kavita.Models.Entities.User; +using Microsoft.Extensions.Logging; + +namespace Kavita.Services; + +public class RatingService(IUnitOfWork unitOfWork, IScrobblingService scrobblingService, ILogger logger) + : IRatingService +{ + public async Task UpdateSeriesRating(AppUser user, UpdateRatingDto updateRatingDto, + CancellationToken ct = default) + { + var userRating = + await unitOfWork.UserRepository.GetUserRatingAsync(updateRatingDto.SeriesId, user.Id, ct) ?? + new AppUserRating(); + + try + { + userRating.Rating = Math.Clamp(updateRatingDto.UserRating, 0f, 5f); + userRating.HasBeenRated = true; + userRating.SeriesId = updateRatingDto.SeriesId; + + if (userRating.Id == 0) + { + user.Ratings ??= new List(); + user.Ratings.Add(userRating); + } + + unitOfWork.UserRepository.Update(user); + + if (!unitOfWork.HasChanges() || await unitOfWork.CommitAsync(ct)) + { + BackgroundJob.Enqueue(() => + scrobblingService.ScrobbleRatingUpdate(user.Id, updateRatingDto.SeriesId, + userRating.Rating)); + return true; + } + } + catch (Exception ex) + { + logger.LogError(ex, "There was an exception saving rating"); + } + + await unitOfWork.RollbackAsync(ct); + user.Ratings?.Remove(userRating); + + return false; + } + + public async Task UpdateChapterRating(AppUser user, UpdateRatingDto updateRatingDto, + CancellationToken ct = default) + { + if (updateRatingDto.ChapterId == null) + { + return false; + } + + var userRating = + await unitOfWork.UserRepository.GetUserChapterRatingAsync(user.Id, updateRatingDto.ChapterId.Value, ct) ?? + new AppUserChapterRating(); + + try + { + userRating.Rating = Math.Clamp(updateRatingDto.UserRating, 0f, 5f); + userRating.HasBeenRated = true; + userRating.SeriesId = updateRatingDto.SeriesId; + userRating.ChapterId = updateRatingDto.ChapterId.Value; + + if (userRating.Id == 0) + { + user.ChapterRatings ??= new List(); + user.ChapterRatings.Add(userRating); + } + + unitOfWork.UserRepository.Update(user); + + await unitOfWork.CommitAsync(ct); + return true; + } + catch (Exception ex) + { + logger.LogError(ex, "There was an exception saving rating"); + } + + await unitOfWork.RollbackAsync(ct); + user.ChapterRatings?.Remove(userRating); + + return false; + } + +} diff --git a/API/Services/Reading/ReaderService.cs b/Kavita.Services/Reading/ReaderService.cs similarity index 95% rename from API/Services/Reading/ReaderService.cs rename to Kavita.Services/Reading/ReaderService.cs index 34b9769a6..bb729fb27 100644 --- a/API/Services/Reading/ReaderService.cs +++ b/Kavita.Services/Reading/ReaderService.cs @@ -3,47 +3,29 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Comparators; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Progress; -using API.DTOs.Reader; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Progress; -using API.Entities.User; -using API.Extensions; -using API.Helpers.Formatting; -using API.Services.Plus; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.DTOs.Reader; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; +using Kavita.Services.Comparators; +using Kavita.Services.Extensions; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; -namespace API.Services.Reading; -#nullable enable - -public interface IReaderService -{ - Task MarkSeriesAsRead(AppUser user, int seriesId); - Task MarkSeriesAsUnread(AppUser user, int seriesId); - Task MarkChaptersAsRead(AppUser user, int seriesId, IList chapters); - Task MarkChaptersAsUnread(AppUser user, int seriesId, IList chapters); - Task SaveReadingProgress(ProgressDto progressDto, int userId); - int CapPageToChapter(Chapter chapter, int page); - Task GetNextChapterIdAsync(int seriesId, int volumeId, int currentChapterId, int userId); - Task GetPrevChapterIdAsync(int seriesId, int volumeId, int currentChapterId, int userId); - Task GetContinuePoint(int seriesId, int userId); - Task MarkChaptersUntilAsRead(AppUser user, int seriesId, float chapterNumber); - Task MarkVolumesUntilAsRead(AppUser user, int seriesId, int volumeNumber); - IDictionary GetPairs(IEnumerable dimensions); - Task GetThumbnail(Chapter chapter, int pageNum, IEnumerable cachedImages); - Task CheckSeriesForReRead(int userId, int seriesId, int libraryId); - Task CheckVolumeForReRead(int userId, int volumeId, int seriesId, int libraryId); - Task CheckChapterForReRead(int userId, int chapterId, int seriesId, int libraryId); -} +namespace Kavita.Services.Reading; public class ReaderService(IUnitOfWork unitOfWork, ILogger logger, IEventHub eventHub, IImageService imageService, IDirectoryService directoryService, IScrobblingService scrobblingService, IReadingSessionService readingSessionService, @@ -55,12 +37,12 @@ public class ReaderService(IUnitOfWork unitOfWork, ILogger logger private readonly ChapterSortComparerDefaultFirst _chapterSortComparerForInChapterSorting = ChapterSortComparerDefaultFirst.Default; private readonly ChapterSortComparerSpecialsLast _chapterSortComparerSpecialsLast = ChapterSortComparerSpecialsLast.Default; - private const float MinWordsPerHour = 10260F; - private const float MaxWordsPerHour = 30000F; - public const float AvgWordsPerHour = (MaxWordsPerHour + MinWordsPerHour) / 2F; - private const float MinPagesPerMinute = 3.33F; - private const float MaxPagesPerMinute = 2.75F; - public const float AvgPagesPerMinute = (MaxPagesPerMinute + MinPagesPerMinute) / 2F; //3.04 + private const float MinWordsPerHour = IReaderService.MinWordsPerHour; + private const float MaxWordsPerHour = IReaderService.MaxWordsPerHour; + private const float MinPagesPerMinute = IReaderService.MinPagesPerMinute; + private const float MaxPagesPerMinute = IReaderService.MaxPagesPerMinute; + public const float AvgWordsPerHour = IReaderService.AvgWordsPerHour; + public const float AvgPagesPerMinute = IReaderService.AvgWordsPerHour; public static string FormatBookmarkFolderPath(string baseDirectory, int userId, int seriesId, int chapterId) diff --git a/API/Services/Reading/ReadingHistoryService.cs b/Kavita.Services/Reading/ReadingHistoryService.cs similarity index 84% rename from API/Services/Reading/ReadingHistoryService.cs rename to Kavita.Services/Reading/ReadingHistoryService.cs index 6b7330ca9..f44a8ee31 100644 --- a/API/Services/Reading/ReadingHistoryService.cs +++ b/Kavita.Services/Reading/ReadingHistoryService.cs @@ -1,77 +1,66 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.DTOs.Progress; -using API.Entities.Enums; -using API.Entities.Progress; +using Kavita.API.Database; +using Kavita.API.Services.Reading; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Services.Reading; -#nullable enable +namespace Kavita.Services.Reading; -public interface IReadingHistoryService +public class ReadingHistoryService(IDataContext context, ILogger logger) + : IReadingHistoryService { - Task AggregateYesterdaysActivity(); -} - -public class ReadingHistoryService : IReadingHistoryService -{ - private readonly DataContext _context; - private readonly ILogger _logger; private sealed record ChapterMetadata(int Id, string? Range, float VolumeNumber, string SeriesName, string? LocalizedSeriesName, string LibraryName, LibraryType LibraryType); private sealed record SeriesMetadata(int Id, string Name, string? LocalizedName, string LibraryName, LibraryType LibraryType); - public ReadingHistoryService(DataContext context, ILogger logger) - { - _context = context; - _logger = logger; - } - - public async Task AggregateYesterdaysActivity() + public async Task AggregateYesterdaysActivity(CancellationToken ct = default) { var yesterdayUtc = DateTime.UtcNow.Date.AddDays(-1); var startUtc = yesterdayUtc; var endUtc = yesterdayUtc.AddDays(1).AddTicks(-1); - var usersToProcess = await GetUsersPendingAggregation(startUtc, endUtc, yesterdayUtc); + var usersToProcess = await GetUsersPendingAggregation(startUtc, endUtc, yesterdayUtc, ct); foreach (var userId in usersToProcess) { - await AggregateUserActivity(userId, startUtc, endUtc, yesterdayUtc); + await AggregateUserActivity(userId, startUtc, endUtc, yesterdayUtc, ct); } - await _context.SaveChangesAsync(); + await context.SaveChangesAsync(ct); } - private async Task> GetUsersPendingAggregation(DateTime start, DateTime end, DateTime reportDate) + private async Task> GetUsersPendingAggregation(DateTime start, DateTime end, DateTime reportDate, CancellationToken ct = default) { - var needAggregationUserIds = await _context.AppUserReadingSession + var needAggregationUserIds = await context.AppUserReadingSession .Where(s => s.StartTime >= start && s.StartTime <= end) .Where(s => !s.IsActive && s.EndTime != null) .Select(s => s.AppUserId) .Distinct() - .ToListAsync(); + .ToListAsync(ct); - var alreadyHasHistoryUserIds = await _context.AppUserReadingHistory + var alreadyHasHistoryUserIds = await context.AppUserReadingHistory .Where(h => h.DateUtc == reportDate) .Select(h => h.AppUserId) - .ToListAsync(); + .ToListAsync(ct); return needAggregationUserIds.Except(alreadyHasHistoryUserIds).ToList(); } - private async Task AggregateUserActivity(int userId, DateTime start, DateTime end, DateTime reportDate) + private async Task AggregateUserActivity(int userId, DateTime start, DateTime end, DateTime reportDate, CancellationToken ct = default) { - var sessions = await _context.AppUserReadingSession + var sessions = await context.AppUserReadingSession .Include(s => s.ActivityData) .Where(s => s.AppUserId == userId && s.StartTime >= start && s.StartTime <= end && !s.IsActive && s.EndTime != null) - .ToListAsync(); + .ToListAsync(ct); if (sessions.Count == 0) return; @@ -80,7 +69,7 @@ public class ReadingHistoryService : IReadingHistoryService var dailyData = CalculateDailyData(sessions, chapterMeta, seriesMeta); - _context.AppUserReadingHistory.Add(new AppUserReadingHistory + context.AppUserReadingHistory.Add(new AppUserReadingHistory { AppUserId = userId, DateUtc = reportDate, @@ -92,7 +81,7 @@ public class ReadingHistoryService : IReadingHistoryService private async Task> GetChapterMetadata(List sessions) { var ids = sessions.SelectMany(s => s.ActivityData.Select(ad => ad.ChapterId)).Distinct().ToList(); - return await _context.Chapter + return await context.Chapter .Where(c => ids.Contains(c.Id)) .Select(c => new ChapterMetadata( c.Id, c.Range, c.Volume.MinNumber, c.Volume.Series.Name, @@ -104,7 +93,7 @@ public class ReadingHistoryService : IReadingHistoryService private async Task> GetSeriesMetadata(List sessions) { var ids = sessions.SelectMany(s => s.ActivityData.Select(ad => ad.SeriesId)).Distinct().ToList(); - return await _context.Series + return await context.Series .Where(s => ids.Contains(s.Id)) .Select(s => new SeriesMetadata(s.Id, s.Name, s.LocalizedName, s.Library.Name, s.Library.Type)) .ToDictionaryAsync(s => s.Id); diff --git a/API/Services/ReadingItemService.cs b/Kavita.Services/Reading/ReadingItemService.cs similarity index 92% rename from API/Services/ReadingItemService.cs rename to Kavita.Services/Reading/ReadingItemService.cs index 543fbaedb..ce7cc0f63 100644 --- a/API/Services/ReadingItemService.cs +++ b/Kavita.Services/Reading/ReadingItemService.cs @@ -1,19 +1,12 @@ using System; -using API.Data.Metadata; -using API.Entities.Enums; -using API.Services.Tasks.Scanner.Parser; +using Kavita.API.Services; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Models.Parser; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; -namespace API.Services; -#nullable enable - -public interface IReadingItemService -{ - int GetNumberOfPages(string filePath, MangaFormat format); - string GetCoverImage(string filePath, string fileName, MangaFormat format, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); - void Extract(string fileFilePath, string targetDirectory, MangaFormat format, int imageCount = 1); - ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata); -} +namespace Kavita.Services.Reading; public class ReadingItemService : IReadingItemService { diff --git a/API/Services/ReadingListService.cs b/Kavita.Services/Reading/ReadingListService.cs similarity index 75% rename from API/Services/ReadingListService.cs rename to Kavita.Services/Reading/ReadingListService.cs index 0ba617b98..b4fd5dbcb 100644 --- a/API/Services/ReadingListService.cs +++ b/Kavita.Services/Reading/ReadingListService.cs @@ -6,91 +6,45 @@ using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Xml.Serialization; -using API.Data; -using API.Data.Repositories; -using API.DTOs.ReadingLists; -using API.DTOs.ReadingLists.CBL; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers; -using API.Helpers.Builders; -using API.Services.Reading; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.DTOs.ReadingLists.CBL; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Models.Extensions; +using Kavita.Models.Helpers; +using Kavita.Services.Extensions; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; -namespace API.Services; -#nullable enable - -public interface IReadingListService -{ - Task CreateReadingListForUser(AppUser userWithReadingList, string title); - Task UpdateReadingList(ReadingList readingList, UpdateReadingListDto dto); - Task RemoveFullyReadItems(int readingListId, AppUser user); - Task UpdateReadingListItemPosition(UpdateReadingListPosition dto); - Task DeleteReadingListItem(UpdateReadingListPosition dto); - Task UserHasReadingListAccess(int readingListId, string username); - Task DeleteReadingList(int readingListId, AppUser user); - Task CalculateReadingListAgeRating(ReadingList readingList); - Task AddChaptersToReadingList(int seriesId, IList chapterIds, - ReadingList readingList); - - Task ValidateCblFile(int userId, CblReadingList cblReading, bool useComicLibraryMatching = false); - Task CreateReadingListFromCbl(int userId, CblReadingList cblReading, bool dryRun = false, bool useComicLibraryMatching = false); - Task CalculateStartAndEndDates(ReadingList readingListWithItems); - /// - /// This is expected to be called from ProcessSeries and has the Full Series present. Will generate on the default admin user. - /// - /// - /// - /// - Task CreateReadingListsFromSeries(Series series, Library library); - - Task CreateReadingListsFromSeries(int libraryId, int seriesId); - Task GenerateReadingListCoverImage(int readingListId); - /// - /// Check, and update if needed, all reading lists' AgeRating who contain the passed series - /// - /// The series whose age rating is being updated - /// The new (uncommited) age rating of the series - /// - /// This method does not commit changes - Task UpdateReadingListAgeRatingForSeries(int seriesId, AgeRating ageRating); - - Task> GetReadingListItems(int readingListId, int userId, UserParams? userParams = null); - Task GetContinueReadingPoint(int readingListId, int userId); -} +namespace Kavita.Services.Reading; /// /// Methods responsible for management of Reading Lists /// /// If called from API layer, expected for to be called beforehand -public class ReadingListService : IReadingListService +public class ReadingListService( + IUnitOfWork unitOfWork, + ILogger logger, + IEventHub eventHub, + IImageService imageService, + IDirectoryService directoryService, + IEntityNamingService namingService) + : IReadingListService { - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - private readonly IEventHub _eventHub; - private readonly IImageService _imageService; - private readonly IDirectoryService _directoryService; - private readonly IEntityNamingService _namingService; - private static readonly Regex JustNumbers = new Regex(@"^\d+$", RegexOptions.Compiled | RegexOptions.IgnoreCase, Parser.RegexTimeout); - public ReadingListService(IUnitOfWork unitOfWork, ILogger logger, - IEventHub eventHub, IImageService imageService, IDirectoryService directoryService, - IEntityNamingService namingService) - { - _unitOfWork = unitOfWork; - _logger = logger; - _eventHub = eventHub; - _imageService = imageService; - _directoryService = directoryService; - _namingService = namingService; - } - public static string FormatTitle(ReadingListItemDto item) { var title = string.Empty; @@ -166,8 +120,8 @@ public class ReadingListService : IReadingListService public async Task CreateReadingListForUser(AppUser userWithReadingList, string title) { // When creating, we need to make sure Title is unique - // TODO: Perform normalization - var hasExisting = userWithReadingList.ReadingLists.Any(l => l.Title.Equals(title)); + var normalizedTitle = title.ToNormalized(); + var hasExisting = userWithReadingList.ReadingLists.Any(l => l.NormalizedTitle == normalizedTitle); if (hasExisting) { throw new KavitaException("reading-list-name-exists"); @@ -176,8 +130,8 @@ public class ReadingListService : IReadingListService var readingList = new ReadingListBuilder(title).Build(); userWithReadingList.ReadingLists.Add(readingList); - if (!_unitOfWork.HasChanges()) throw new KavitaException("generic-reading-list-create"); - await _unitOfWork.CommitAsync(); + if (!unitOfWork.HasChanges()) throw new KavitaException("generic-reading-list-create"); + await unitOfWork.CommitAsync(); return readingList; } @@ -192,7 +146,7 @@ public class ReadingListService : IReadingListService dto.Title = dto.Title.Trim(); if (string.IsNullOrEmpty(dto.Title)) throw new KavitaException("reading-list-title-required"); - if (!dto.Title.Equals(readingList.Title) && await _unitOfWork.ReadingListRepository.ReadingListExists(dto.Title)) + if (!dto.Title.Equals(readingList.Title) && await unitOfWork.ReadingListRepository.ReadingListExists(dto.Title, readingList.Id)) throw new KavitaException("reading-list-name-exists"); readingList.Summary = dto.Summary; @@ -224,15 +178,15 @@ public class ReadingListService : IReadingListService { readingList.CoverImageLocked = false; readingList.CoverImage = string.Empty; - await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, + await eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(readingList.Id, MessageFactoryEntityTypes.ReadingList), false); - _unitOfWork.ReadingListRepository.Update(readingList); + unitOfWork.ReadingListRepository.Update(readingList); } - _unitOfWork.ReadingListRepository.Update(readingList); + unitOfWork.ReadingListRepository.Update(readingList); - if (!_unitOfWork.HasChanges()) return; - await _unitOfWork.CommitAsync(); + if (!unitOfWork.HasChanges()) return; + await unitOfWork.CommitAsync(); } /// @@ -244,7 +198,7 @@ public class ReadingListService : IReadingListService /// public async Task RemoveFullyReadItems(int readingListId, AppUser user) { - var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, user.Id); + var items = await unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, user.Id); // Collect all Ids to remove var itemIdsToRemove = items.Where(item => item.PagesRead == item.PagesTotal).Select(item => item.Id).ToList(); @@ -253,21 +207,21 @@ public class ReadingListService : IReadingListService try { var listItems = - (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).Where(r => + (await unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).Where(r => itemIdsToRemove.Contains(r.Id)); - _unitOfWork.ReadingListRepository.BulkRemove(listItems); + unitOfWork.ReadingListRepository.BulkRemove(listItems); - var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListId); + var readingList = await unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListId); if (readingList == null) return true; await CalculateReadingListAgeRating(readingList); await CalculateStartAndEndDates(readingList); - if (!_unitOfWork.HasChanges()) return true; - return await _unitOfWork.CommitAsync(); + if (!unitOfWork.HasChanges()) return true; + return await unitOfWork.CommitAsync(); } catch { - await _unitOfWork.RollbackAsync(); + await unitOfWork.RollbackAsync(); } return false; @@ -280,12 +234,12 @@ public class ReadingListService : IReadingListService /// public async Task UpdateReadingListItemPosition(UpdateReadingListPosition dto) { - var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(dto.ReadingListId)).ToList(); + var items = (await unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(dto.ReadingListId)).ToList(); OrderableHelper.ReorderItems(items, dto.ReadingListItemId, dto.ToPosition); - if (!_unitOfWork.HasChanges()) return true; + if (!unitOfWork.HasChanges()) return true; - return await _unitOfWork.CommitAsync(); + return await unitOfWork.CommitAsync(); } /// @@ -295,7 +249,7 @@ public class ReadingListService : IReadingListService /// public async Task DeleteReadingListItem(UpdateReadingListPosition dto) { - var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId); + var readingList = await unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId); if (readingList == null) return false; readingList.Items = readingList.Items.Where(r => r.Id != dto.ReadingListItemId).OrderBy(r => r.Order).ToList(); @@ -309,9 +263,9 @@ public class ReadingListService : IReadingListService await CalculateReadingListAgeRating(readingList); await CalculateStartAndEndDates(readingList); - if (!_unitOfWork.HasChanges()) return true; + if (!unitOfWork.HasChanges()) return true; - return await _unitOfWork.CommitAsync(); + return await unitOfWork.CommitAsync(); } /// @@ -333,13 +287,13 @@ public class ReadingListService : IReadingListService if (readingListWithItems.Items.All(i => i.Chapter == null)) { items = - (await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListWithItems.Id, ReadingListIncludes.ItemChapter))?.Items; + (await unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListWithItems.Id, ReadingListIncludes.ItemChapter))?.Items; } if (items == null || items.Count == 0) return; if (items.First().Chapter == null) { - _logger.LogError("Tried to calculate release dates for Reading List, but missing Chapter entities"); + logger.LogError("Tried to calculate release dates for Reading List, but missing Chapter entities"); return; } var maxReleaseDate = items.Where(item => item.Chapter != null).Max(item => item.Chapter.ReleaseDate); @@ -364,7 +318,7 @@ public class ReadingListService : IReadingListService /// The series ids of all the reading list items private async Task CalculateReadingListAgeRating(ReadingList readingList, IEnumerable seriesIds) { - var ageRating = await _unitOfWork.SeriesRepository.GetMaxAgeRatingFromSeriesAsync(seriesIds); + var ageRating = await unitOfWork.SeriesRepository.GetMaxAgeRatingFromSeriesAsync(seriesIds); readingList.AgeRating = ageRating; } @@ -377,7 +331,7 @@ public class ReadingListService : IReadingListService public async Task UserHasReadingListAccess(int readingListId, string username) { // We need full reading list with items as this is used by many areas that manipulate items - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(username, + var user = await unitOfWork.UserRepository.GetUserByUsernameAsync(username, AppUserIncludes.ReadingListsWithItems); if (user == null || !await UserHasReadingListAccess(readingListId, user)) { @@ -395,7 +349,7 @@ public class ReadingListService : IReadingListService /// private async Task UserHasReadingListAccess(int readingListId, AppUser user) { - return user.ReadingLists.Any(rl => rl.Id == readingListId) || await _unitOfWork.UserRepository.IsUserAdminAsync(user); + return user.ReadingLists.Any(rl => rl.Id == readingListId) || await unitOfWork.UserRepository.IsUserAdminAsync(user); } /// @@ -406,13 +360,13 @@ public class ReadingListService : IReadingListService /// public async Task DeleteReadingList(int readingListId, AppUser user) { - var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListId); + var readingList = await unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListId); if (readingList == null) return true; user.ReadingLists.Remove(readingList); - if (!_unitOfWork.HasChanges()) return true; + if (!unitOfWork.HasChanges()) return true; - return await _unitOfWork.CommitAsync(); + return await unitOfWork.CommitAsync(); } /// @@ -432,7 +386,7 @@ public class ReadingListService : IReadingListService } var existingChapterExists = readingList.Items.Select(rli => rli.ChapterId).ToHashSet(); - var chaptersForSeries = (await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds, ChapterIncludes.Volumes)) + var chaptersForSeries = (await unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds, ChapterIncludes.Volumes)) .OrderBy(c => c.Volume.MinNumber) .ThenBy(x => x.SortOrder) .ToList(); @@ -457,8 +411,8 @@ public class ReadingListService : IReadingListService /// public async Task CreateReadingListsFromSeries(int libraryId, int seriesId) { - var series = await _unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(seriesId); - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); + var series = await unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(seriesId); + var library = await unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); if (series == null || library == null) return; await CreateReadingListsFromSeries(series, library); @@ -474,8 +428,8 @@ public class ReadingListService : IReadingListService if (!hasReadingListMarkers) return; - _logger.LogInformation("Processing Reading Lists for {SeriesName}", series.Name); - var user = await _unitOfWork.UserRepository.GetDefaultAdminUser(); + logger.LogInformation("Processing Reading Lists for {SeriesName}", series.Name); + var user = await unitOfWork.UserRepository.GetDefaultAdminUser(); series.Metadata ??= new SeriesMetadataBuilder().Build(); foreach (var chapter in series.Volumes.SelectMany(v => v.Chapters)) @@ -492,13 +446,13 @@ public class ReadingListService : IReadingListService foreach (var arcPair in pairs) { - var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByTitleAsync(arcPair.Item1, user.Id); + var readingList = await unitOfWork.ReadingListRepository.GetReadingListByTitleAsync(arcPair.Item1, user.Id); if (readingList == null) { readingList = new ReadingListBuilder(arcPair.Item1) .WithAppUserId(user.Id) .Build(); - _unitOfWork.ReadingListRepository.Add(readingList); + unitOfWork.ReadingListRepository.Add(readingList); } @@ -518,7 +472,7 @@ public class ReadingListService : IReadingListService { if (order == int.MaxValue) { - _logger.LogWarning("{Filename} has a missing StoryArcNumber/AlternativeNumber but list already exists with this item. Skipping item", chapter.Files.FirstOrDefault()?.FilePath); + logger.LogWarning("{Filename} has a missing StoryArcNumber/AlternativeNumber but list already exists with this item. Skipping item", chapter.Files.FirstOrDefault()?.FilePath); } else { @@ -528,17 +482,17 @@ public class ReadingListService : IReadingListService readingList.Items = items; - if (!_unitOfWork.HasChanges()) continue; + if (!unitOfWork.HasChanges()) continue; - _imageService.UpdateColorScape(readingList); + imageService.UpdateColorScape(readingList); await CalculateReadingListAgeRating(readingList); - await _unitOfWork.CommitAsync(); // TODO: See if we can avoid this extra commit by reworking bottom logic + await unitOfWork.CommitAsync(); // TODO: See if we can avoid this extra commit by reworking bottom logic - await CalculateStartAndEndDates(await _unitOfWork.ReadingListRepository.GetReadingListByTitleAsync(arcPair.Item1, - user.Id, ReadingListIncludes.Items | ReadingListIncludes.ItemChapter)); - await _unitOfWork.CommitAsync(); + await CalculateStartAndEndDates((await unitOfWork.ReadingListRepository.GetReadingListByTitleAsync( + arcPair.Item1, user.Id, ReadingListIncludes.Items | ReadingListIncludes.ItemChapter))!); + await unitOfWork.CommitAsync(); } } } @@ -552,7 +506,7 @@ public class ReadingListService : IReadingListService var arcNumbers = storyArcNumbers.Split(",", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); if (arcNumbers.Count(s => !string.IsNullOrEmpty(s)) != arcs.Length) { - _logger.LogWarning("There is a mismatch on StoryArc and StoryArcNumber for {FileName}", filename); + logger.LogWarning("There is a mismatch on StoryArc and StoryArcNumber for {FileName}", filename); } var maxPairs = Math.Max(arcs.Length, arcNumbers.Length); @@ -590,7 +544,7 @@ public class ReadingListService : IReadingListService if (IsCblEmpty(cblReading, importSummary, out var readingListFromCbl)) return readingListFromCbl; // Is there another reading list with the same name on the user's account? - if (await _unitOfWork.ReadingListRepository.ReadingListExistsForUser(cblReading.Name, userId)) + if (await unitOfWork.ReadingListRepository.ReadingListExistsForUser(cblReading.Name, userId)) { importSummary.Success = CblImportResult.Fail; importSummary.Results.Add(new CblBookResult @@ -603,7 +557,7 @@ public class ReadingListService : IReadingListService var uniqueSeries = GetUniqueSeries(cblReading, useComicLibraryMatching); var userSeries = - (await _unitOfWork.SeriesRepository.GetAllSeriesByNameAsync(uniqueSeries, userId, SeriesIncludes.Chapters)).ToList(); + (await unitOfWork.SeriesRepository.GetAllSeriesByNameAsync(uniqueSeries, userId, SeriesIncludes.Chapters)).ToList(); if (userSeries.Count == 0) { @@ -655,8 +609,8 @@ public class ReadingListService : IReadingListService /// public async Task CreateReadingListFromCbl(int userId, CblReadingList cblReading, bool dryRun = false, bool useComicLibraryMatching = false) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.ReadingListsWithItems); - _logger.LogDebug("Importing {ReadingListName} CBL for User {UserName}", cblReading.Name, user!.UserName); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.ReadingListsWithItems); + logger.LogDebug("Importing {ReadingListName} CBL for User {UserName}", cblReading.Name, user!.UserName); var importSummary = new CblImportSummaryDto { CblName = cblReading.Name, @@ -667,7 +621,7 @@ public class ReadingListService : IReadingListService var uniqueSeries = GetUniqueSeries(cblReading, useComicLibraryMatching); var userSeries = - (await _unitOfWork.SeriesRepository.GetAllSeriesByNameAsync(uniqueSeries, userId, SeriesIncludes.Chapters)).ToList(); + (await unitOfWork.SeriesRepository.GetAllSeriesByNameAsync(uniqueSeries, userId, SeriesIncludes.Chapters)).ToList(); var allSeries = userSeries.ToDictionary(s => s.NormalizedName); var allSeriesLocalized = userSeries.ToDictionary(s => s.NormalizedLocalizedName); @@ -777,11 +731,11 @@ public class ReadingListService : IReadingListService } // If there are no items, don't create a blank list - if (!_unitOfWork.HasChanges() || readingList.Items.Count == 0) return importSummary; + if (!unitOfWork.HasChanges() || readingList.Items.Count == 0) return importSummary; - _imageService.UpdateColorScape(readingList); - await _unitOfWork.CommitAsync(); + imageService.UpdateColorScape(readingList); + await unitOfWork.CommitAsync(); return importSummary; @@ -845,31 +799,31 @@ public class ReadingListService : IReadingListService // // } - var covers = await _unitOfWork.ReadingListRepository.GetRandomCoverImagesAsync(readingListId); - var destFile = _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, + var covers = await unitOfWork.ReadingListRepository.GetRandomCoverImagesAsync(readingListId); + var destFile = directoryService.FileSystem.Path.Join(directoryService.TempDirectory, ImageService.GetReadingListFormat(readingListId)); - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); destFile += settings.EncodeMediaAs.GetExtension(); - if (_directoryService.FileSystem.File.Exists(destFile)) return destFile; + if (directoryService.FileSystem.File.Exists(destFile)) return destFile; ImageService.CreateMergedImage( - covers.Select(c => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, c)).ToList(), + covers.Select(c => directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory, c)).ToList(), settings.CoverImageSize, destFile); // TODO: Refactor this so that reading lists have a dedicated cover image so we can calculate primary/secondary colors - return !_directoryService.FileSystem.File.Exists(destFile) ? string.Empty : destFile; + return !directoryService.FileSystem.File.Exists(destFile) ? string.Empty : destFile; } public async Task UpdateReadingListAgeRatingForSeries(int seriesId, AgeRating ageRating) { - var readingLists = await _unitOfWork.ReadingListRepository.GetReadingListsBySeriesId(seriesId); + var readingLists = await unitOfWork.ReadingListRepository.GetReadingListsBySeriesId(seriesId); foreach (var readingList in readingLists) { var seriesIds = readingList.Items.Select(item => item.SeriesId).ToList(); seriesIds.Remove(seriesId); // Don't get AgeRating from database - var maxAgeRating = await _unitOfWork.SeriesRepository.GetMaxAgeRatingFromSeriesAsync(seriesIds); + var maxAgeRating = await unitOfWork.SeriesRepository.GetMaxAgeRatingFromSeriesAsync(seriesIds); if (ageRating > maxAgeRating) { maxAgeRating = ageRating; @@ -881,12 +835,12 @@ public class ReadingListService : IReadingListService public async Task> GetReadingListItems(int readingListId, int userId, UserParams? userParams = null) { - var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId, userParams); + var items = await unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId, userParams); // Add the title foreach (var item in items) { - item.Title = _namingService.FormatReadingListItemTitle(item); + item.Title = namingService.FormatReadingListItemTitle(item); } return items; @@ -894,8 +848,8 @@ public class ReadingListService : IReadingListService public async Task GetContinueReadingPoint(int readingListId, int userId) { - var item = await _unitOfWork.ReadingListRepository.GetContinueReadingPoint(readingListId, userId); - item?.Title = _namingService.FormatReadingListItemTitle(item); + var item = await unitOfWork.ReadingListRepository.GetContinueReadingPoint(readingListId, userId); + item?.Title = namingService.FormatReadingListItemTitle(item); return item; } diff --git a/API/Services/ReadingProfileService.cs b/Kavita.Services/Reading/ReadingProfileService.cs similarity index 74% rename from API/Services/ReadingProfileService.cs rename to Kavita.Services/Reading/ReadingProfileService.cs index 6726dbfb4..09ae8b597 100644 --- a/API/Services/ReadingProfileService.cs +++ b/Kavita.Services/Reading/ReadingProfileService.cs @@ -2,171 +2,21 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Helpers.Builders; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Reading; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models.Builders; +using Kavita.Models.DTOs; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; using Microsoft.EntityFrameworkCore; -namespace API.Services; -#nullable enable - -public interface IReadingProfileService -{ - /// - /// Returns the ReadingProfile that should be applied to the given series, walks up the tree. - /// Series (Implicit) -> Series (User) -> Library (User) -> Default - /// - /// - /// - /// - /// - /// - /// - Task GetReadingProfileDtoForSeries(int userId, int libraryId, int seriesId, int? activeDeviceId, bool skipImplicit = false); - - /// - /// Creates a new reading profile for a user. Name must be unique per user - /// - /// - /// - /// - Task CreateReadingProfile(int userId, UserReadingProfileDto dto); - /// - /// Given an implicit profile, promotes it to a profile of kind , then removes - /// all links to the series this implicit profile was created for from other reading profiles (if the device id matches - /// if given) - /// - /// - /// - /// - /// - Task PromoteImplicitProfile(int userId, int profileId, int? activeDeviceId); - - /// - /// Updates the implicit reading profile for a series, creates one if none exists - /// - /// - /// - /// - /// - /// - /// - Task UpdateImplicitReadingProfile(int userId, int libraryId, int seriesId, UserReadingProfileDto dto, int? activeDeviceId); - - /// - /// Updates the non-implicit reading profile for the given series, and removes implicit profiles - /// - /// - /// - /// - /// - /// - /// - Task UpdateParent(int userId, int libraryId, int seriesId, UserReadingProfileDto dto, int? activeDeviceId); - - /// - /// Updates a given reading profile for a user - /// - /// - /// - /// - /// Does not update connected series and libraries - Task UpdateReadingProfile(int userId, UserReadingProfileDto dto); - - /// - /// Deletes a given profile for a user - /// - /// - /// - /// - /// - /// The default profile for the user cannot be deleted - Task DeleteReadingProfile(int userId, int profileId); - - /// - /// Binds the reading profile to the series, and remove the implicit RP from the series if it exists - /// - /// - /// - /// - /// - Task SetSeriesProfiles(int userId, List profileIds, int seriesId); - - /// - /// Binds the reading profile to many series, and remove the implicit RP from the series if it exists - /// - /// - /// - /// - /// - Task BulkSetSeriesProfiles(int userId, List profileIds, List seriesIds); - - /// - /// Remove all reading profiles bound to the series - /// - /// - /// - /// - Task ClearSeriesProfile(int userId, int seriesId); - - /// - /// Bind the reading profile to the library - /// - /// - /// - /// - /// - Task SetLibraryProfiles(int userId, List profileIds, int libraryId); - - /// - /// Remove the reading profile bound to the library, if it exists - /// - /// - /// - /// - Task ClearLibraryProfile(int userId, int libraryId); - - /// - /// Returns the all bound Reading Profile to a Library - /// - /// - /// - /// - Task> GetReadingProfileDtosForLibrary(int userId, int libraryId); - - /// - /// Returns the all bound Reading Profile to a Series - /// - /// - /// - /// - Task> GetReadingProfileDtosForSeries(int userId, int seriesId); - - /// - /// Set the assigned devices for the given reading profile. Then removes all duplicate links, ensuring each series - /// and library only has one profile per device - /// - /// - /// - /// - /// - Task SetProfileDevices(int userId, int profileId, List deviceIds); - - /// - /// Remove device ids from all profiles, does **NOT** commit - /// - /// - /// - /// - Task RemoveDeviceLinks(int userId, int deviceId); -} +namespace Kavita.Services.Reading; public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService localizationService, IMapper mapper): IReadingProfileService { diff --git a/API/Services/Reading/ReadingSessionService.cs b/Kavita.Services/Reading/ReadingSessionService.cs similarity index 96% rename from API/Services/Reading/ReadingSessionService.cs rename to Kavita.Services/Reading/ReadingSessionService.cs index b921b8658..d257b74de 100644 --- a/API/Services/Reading/ReadingSessionService.cs +++ b/Kavita.Services/Reading/ReadingSessionService.cs @@ -4,25 +4,21 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.DTOs.Progress; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Progress; -using API.SignalR; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; +using Kavita.Models.DTOs.Progress; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Progress; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace API.Services.Reading; - -#nullable enable - -public interface IReadingSessionService -{ - Task UpdateProgress(int userId, ProgressDto progressDto, ClientInfoData? clientInfo, int? deviceId); -} +namespace Kavita.Services.Reading; public sealed class ReadingSessionService : IReadingSessionService, IDisposable, IAsyncDisposable { @@ -74,7 +70,7 @@ public sealed class ReadingSessionService : IReadingSessionService, IDisposable, _logger.LogDebug("Updating Reading Session for {UserId} on {ChapterId}", userId, progressDto.ChapterId); using var scope = _serviceScopeFactory.CreateScope(); - var context = scope.ServiceProvider.GetRequiredService(); + var context = scope.ServiceProvider.GetRequiredService(); var eventHub = scope.ServiceProvider.GetRequiredService(); var session = await GetOrCreateSessionAsync(userId, progressDto, context); @@ -95,7 +91,7 @@ public sealed class ReadingSessionService : IReadingSessionService, IDisposable, } } - private async Task GetOrCreateSessionAsync(int userId, ProgressDto dto, DataContext context) + private async Task GetOrCreateSessionAsync(int userId, ProgressDto dto, IDataContext context) { var cutoffUtc = DateTime.UtcNow - _sessionTimeout; var midnightToday = DateTime.Today; @@ -133,7 +129,7 @@ public sealed class ReadingSessionService : IReadingSessionService, IDisposable, } private async Task UpdateActivityDataAsync(AppUserReadingSession session, ProgressDto progressDto, ClientInfoData? clientInfo, - int? deviceId, IServiceScope scope, DataContext context) + int? deviceId, IServiceScope scope, IDataContext context) { var cutoffUtc = DateTime.UtcNow - _sessionTimeout; @@ -254,7 +250,7 @@ public sealed class ReadingSessionService : IReadingSessionService, IDisposable, var midnightToday = DateTime.Today; using var scope = _serviceScopeFactory.CreateScope(); - var context = scope.ServiceProvider.GetRequiredService(); + var context = scope.ServiceProvider.GetRequiredService(); var eventHub = scope.ServiceProvider.GetRequiredService(); var expiredSessions = await context.AppUserReadingSession @@ -349,7 +345,7 @@ public sealed class ReadingSessionService : IReadingSessionService, IDisposable, return completedChapterIds; } - private async Task GetChapterFormatAsync(int chapterId, DataContext context) + private async Task GetChapterFormatAsync(int chapterId, IDataContext context) { var cacheKey = GetChapterFormatCacheKey(chapterId); diff --git a/Kavita.Services/Repositories/CoverDbRepository.cs b/Kavita.Services/Repositories/CoverDbRepository.cs new file mode 100644 index 000000000..a7b436565 --- /dev/null +++ b/Kavita.Services/Repositories/CoverDbRepository.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Kavita.Models.DTOs.CoverDb; +using Kavita.Models.Entities.Person; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace Kavita.Services.Repositories; + +/// +/// This is a manual repository, not a DB repo +/// +public class CoverDbRepository +{ + private readonly List _authors; + + public CoverDbRepository(string filePath) + { + var deserializer = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + + // Read and deserialize YAML file + var yamlContent = File.ReadAllText(filePath); + var peopleData = deserializer.Deserialize(yamlContent); + _authors = peopleData.People; + } + + public CoverDbAuthor? FindAuthorByNameOrAlias(string name) + { + return _authors.Find(author => + author.Name.Equals(name, StringComparison.OrdinalIgnoreCase) || + author.Aliases.Contains(name, StringComparer.OrdinalIgnoreCase)); + } + + public CoverDbAuthor? FindBestAuthorMatch(Person person) + { + var aniListId = person.AniListId > 0 ? $"{person.AniListId}" : string.Empty; + var highestScore = 0; + CoverDbAuthor? bestMatch = null; + + foreach (var author in _authors) + { + var score = 0; + + // Check metadata IDs and add points if they match + if (!string.IsNullOrEmpty(author.Ids.AmazonId) && author.Ids.AmazonId == person.Asin) + { + score += 10; + } + if (!string.IsNullOrEmpty(author.Ids.AnilistId) && author.Ids.AnilistId == aniListId) + { + score += 10; + } + if (!string.IsNullOrEmpty(author.Ids.HardcoverId) && author.Ids.HardcoverId == person.HardcoverId) + { + score += 10; + } + + // Check for exact name match + if (author.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase)) + { + score += 7; + } + + // Check for alias match + if (author.Aliases.Contains(person.Name, StringComparer.OrdinalIgnoreCase)) + { + score += 5; + } + + // Update the best match if current score is higher + if (score <= highestScore) continue; + + highestScore = score; + bestMatch = author; + } + + return bestMatch; + } + +} diff --git a/API/Services/Tasks/Scanner/Parser/BasicParser.cs b/Kavita.Services/Scanner/BasicParser.cs similarity index 67% rename from API/Services/Tasks/Scanner/Parser/BasicParser.cs rename to Kavita.Services/Scanner/BasicParser.cs index 11cb51bcd..c747c1b67 100644 --- a/API/Services/Tasks/Scanner/Parser/BasicParser.cs +++ b/Kavita.Services/Scanner/BasicParser.cs @@ -1,10 +1,11 @@ using System; using System.IO; -using API.Data.Metadata; -using API.Entities.Enums; +using Kavita.API.Services; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Models.Parser; -namespace API.Services.Tasks.Scanner.Parser; -#nullable enable +namespace Kavita.Services.Scanner; /// /// This is the basic parser for handling Manga/Comic/Book libraries. This was previously DefaultParser before splitting each parser @@ -16,9 +17,9 @@ public class BasicParser(IDirectoryService directoryService, IDefaultParser imag { var fileName = directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath); // TODO: Potential Bug: This will return null, but on Image libraries, if all images, we would want to include this. - if (type != LibraryType.Image && Parser.IsCoverImage(directoryService.FileSystem.Path.GetFileName(filePath))) return null; + if (type != LibraryType.Image && Scanner.Parser.IsCoverImage(directoryService.FileSystem.Path.GetFileName(filePath))) return null; - if (Parser.IsImage(filePath)) + if (Scanner.Parser.IsImage(filePath)) { return imageParser.Parse(filePath, rootPath, libraryRoot, LibraryType.Image, enableMetadata, comicInfo); } @@ -26,44 +27,44 @@ public class BasicParser(IDirectoryService directoryService, IDefaultParser imag var ret = new ParserInfo() { Filename = Path.GetFileName(filePath), - Format = Parser.ParseFormat(filePath), - Title = Parser.RemoveExtensionIfSupported(fileName)!, - FullFilePath = Parser.NormalizePath(filePath), - Series = Parser.ParseSeries(fileName, type), + Format = Scanner.Parser.ParseFormat(filePath), + Title = Scanner.Parser.RemoveExtensionIfSupported(fileName)!, + FullFilePath = Scanner.Parser.NormalizePath(filePath), + Series = Scanner.Parser.ParseSeries(fileName, type), ComicInfo = comicInfo, - Chapters = Parser.ParseChapter(fileName, type), - Volumes = Parser.ParseVolume(fileName, type), + Chapters = Scanner.Parser.ParseChapter(fileName, type), + Volumes = Scanner.Parser.ParseVolume(fileName, type), }; - if (ret.Series == string.Empty || Parser.IsImage(filePath)) + if (ret.Series == string.Empty || Scanner.Parser.IsImage(filePath)) { // Try to parse information out of each folder all the way to rootPath ParseFromFallbackFolders(filePath, rootPath, type, ref ret); } - var edition = Parser.ParseEdition(fileName); + var edition = Scanner.Parser.ParseEdition(fileName); if (!string.IsNullOrEmpty(edition)) { - ret.Series = Parser.CleanTitle(ret.Series.Replace(edition, string.Empty), type is LibraryType.Comic); + ret.Series = Scanner.Parser.CleanTitle(ret.Series.Replace(edition, string.Empty), type is LibraryType.Comic); ret.Edition = edition; } - var isSpecial = Parser.IsSpecial(fileName, type); + var isSpecial = Scanner.Parser.IsSpecial(fileName, type); // We must ensure that we can only parse a special out. As some files will have v20 c171-180+Omake and that // could cause a problem as Omake is a special term, but there is valid volume/chapter information. - if (Parser.IsDefaultChapter(ret.Chapters) && Parser.IsLooseLeafVolume(ret.Volumes) && isSpecial) + if (Scanner.Parser.IsDefaultChapter(ret.Chapters) && Scanner.Parser.IsLooseLeafVolume(ret.Volumes) && isSpecial) { ret.IsSpecial = true; ParseFromFallbackFolders(filePath, rootPath, type, ref ret); // NOTE: This can cause some complications, we should try to be a bit less aggressive to fallback to folder } // If we are a special with marker, we need to ensure we use the correct series name. we can do this by falling back to Folder name - if (Parser.HasSpecialMarker(fileName)) + if (Scanner.Parser.HasSpecialMarker(fileName)) { ret.IsSpecial = true; - ret.SpecialIndex = Parser.ParseSpecialIndex(fileName); - ret.Chapters = Parser.DefaultChapter; - ret.Volumes = Parser.SpecialVolume; + ret.SpecialIndex = Scanner.Parser.ParseSpecialIndex(fileName); + ret.Chapters = Scanner.Parser.DefaultChapter; + ret.Volumes = Scanner.Parser.SpecialVolume; // NOTE: This uses rootPath. LibraryRoot works better for manga, but it's not always that way. // It might be worth writing some logic if the file is a special, to take the folder above the Specials/ @@ -80,22 +81,22 @@ public class BasicParser(IDirectoryService directoryService, IDefaultParser imag (fileDirectory.EndsWith("Specials", StringComparison.OrdinalIgnoreCase) || fileDirectory.EndsWith("Specials/", StringComparison.OrdinalIgnoreCase))) { - ret.Series = Parser.CleanTitle(Directory.GetParent(fileDirectory)?.Name ?? string.Empty); + ret.Series = Scanner.Parser.CleanTitle(Directory.GetParent(fileDirectory)?.Name ?? string.Empty); } else { ParseFromFallbackFolders(filePath, tempRootPath, type, ref ret); } - ret.Title = Parser.CleanSpecialTitle(fileName); + ret.Title = Scanner.Parser.CleanSpecialTitle(fileName); } if (string.IsNullOrEmpty(ret.Series)) { - ret.Series = Parser.CleanTitle(fileName, type is LibraryType.Comic); + ret.Series = Scanner.Parser.CleanTitle(fileName, type is LibraryType.Comic); } // Pdfs may have .pdf in the series name, remove that - if (Parser.IsPdf(filePath) && ret.Series.ToLower().EndsWith(".pdf")) + if (Scanner.Parser.IsPdf(filePath) && ret.Series.ToLower().EndsWith(".pdf")) { ret.Series = ret.Series.Substring(0, ret.Series.Length - ".pdf".Length); } @@ -108,7 +109,7 @@ public class BasicParser(IDirectoryService directoryService, IDefaultParser imag - if (Parser.IsLooseLeafVolume(ret.Volumes) && Parser.IsDefaultChapter(ret.Chapters)) + if (Scanner.Parser.IsLooseLeafVolume(ret.Volumes) && Scanner.Parser.IsDefaultChapter(ret.Chapters)) { ret.IsSpecial = true; } @@ -116,7 +117,7 @@ public class BasicParser(IDirectoryService directoryService, IDefaultParser imag // v0.8.x: Introducing a change where Specials will go in a separate Volume with a reserved number if (ret.IsSpecial) { - ret.Volumes = Parser.SpecialVolume; + ret.Volumes = Scanner.Parser.SpecialVolume; } return ret.Series == string.Empty ? null : ret; diff --git a/API/Services/Tasks/Scanner/Parser/BookParser.cs b/Kavita.Services/Scanner/BookParser.cs similarity index 65% rename from API/Services/Tasks/Scanner/Parser/BookParser.cs rename to Kavita.Services/Scanner/BookParser.cs index 89b142faa..edc9344fb 100644 --- a/API/Services/Tasks/Scanner/Parser/BookParser.cs +++ b/Kavita.Services/Scanner/BookParser.cs @@ -1,8 +1,11 @@ using System.IO; -using API.Data.Metadata; -using API.Entities.Enums; +using Kavita.API.Services; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Models.Parser; +using Kavita.Services.Extensions; -namespace API.Services.Tasks.Scanner.Parser; +namespace Kavita.Services.Scanner; public class BookParser(IDirectoryService directoryService, IBookService bookService, BasicParser basicParser) : DefaultParser(directoryService) { @@ -21,11 +24,11 @@ public class BookParser(IDirectoryService directoryService, IBookService bookSer { Filename = Path.GetFileName(filePath), Format = MangaFormat.Epub, - Title = Parser.RemoveExtensionIfSupported(fileName)!, - FullFilePath = Parser.NormalizePath(filePath), - Series = Parser.ParseSeries(fileName, type), - Chapters = Parser.ParseChapter(fileName, type), - Volumes = Parser.ParseVolume(fileName, type), + Title = Scanner.Parser.RemoveExtensionIfSupported(fileName)!, + FullFilePath = Scanner.Parser.NormalizePath(filePath), + Series = Scanner.Parser.ParseSeries(fileName, type), + Chapters = Scanner.Parser.ParseChapter(fileName, type), + Volumes = Scanner.Parser.ParseVolume(fileName, type), }; } @@ -38,19 +41,19 @@ public class BookParser(IDirectoryService directoryService, IBookService bookSer } // This catches when original library type is Manga/Comic and when parsing with non - if (!Parser.IsLooseLeafVolume(Parser.ParseVolume(info.Series, type))) + if (!Scanner.Parser.IsLooseLeafVolume(Scanner.Parser.ParseVolume(info.Series, type))) { - var parsedVolumeFromTitle = Parser.ParseVolume(info.Title, type); - var parsedVolumeFromSeries = Parser.ParseVolume(info.Series, type); + var parsedVolumeFromTitle = Scanner.Parser.ParseVolume(info.Title, type); + var parsedVolumeFromSeries = Scanner.Parser.ParseVolume(info.Series, type); - var hasVolumeInTitle = !Parser.IsLooseLeafVolume(parsedVolumeFromTitle); - var hasVolumeInSeries = !Parser.IsLooseLeafVolume(parsedVolumeFromSeries); + var hasVolumeInTitle = !Scanner.Parser.IsLooseLeafVolume(parsedVolumeFromTitle); + var hasVolumeInSeries = !Scanner.Parser.IsLooseLeafVolume(parsedVolumeFromSeries); if (string.IsNullOrEmpty(info.ComicInfo?.Volume) && hasVolumeInTitle && (hasVolumeInSeries || string.IsNullOrEmpty(info.Series))) { // NOTE: I'm not sure the comment is true. I've never seen this triggered // This is likely a light novel for which we can set series from parsed title - info.Series = Parser.ParseSeries(info.Title, type); + info.Series = Scanner.Parser.ParseSeries(info.Title, type); info.Volumes = parsedVolumeFromTitle; } else @@ -58,7 +61,7 @@ public class BookParser(IDirectoryService directoryService, IBookService bookSer var info2 = basicParser.Parse(filePath, rootPath, libraryRoot, LibraryType.Book, enableMetadata, comicInfo); info.Merge(info2); - if (hasVolumeInSeries && info2 != null && Parser.IsLooseLeafVolume(Parser.ParseVolume(info2.Series, type))) + if (hasVolumeInSeries && info2 != null && Scanner.Parser.IsLooseLeafVolume(Scanner.Parser.ParseVolume(info2.Series, type))) { // Override the Series name so it groups appropriately info.Series = info2.Series; @@ -77,6 +80,6 @@ public class BookParser(IDirectoryService directoryService, IBookService bookSer /// public override bool IsApplicable(string filePath, LibraryType type) { - return Parser.IsEpub(filePath); + return Scanner.Parser.IsEpub(filePath); } } diff --git a/API/Services/Tasks/Scanner/Parser/ComicVineParser.cs b/Kavita.Services/Scanner/ComicVineParser.cs similarity index 73% rename from API/Services/Tasks/Scanner/Parser/ComicVineParser.cs rename to Kavita.Services/Scanner/ComicVineParser.cs index 4b0878504..a10ad7363 100644 --- a/API/Services/Tasks/Scanner/Parser/ComicVineParser.cs +++ b/Kavita.Services/Scanner/ComicVineParser.cs @@ -1,9 +1,11 @@ using System.IO; using System.Linq; -using API.Data.Metadata; -using API.Entities.Enums; +using Kavita.API.Services; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Models.Parser; -namespace API.Services.Tasks.Scanner.Parser; +namespace Kavita.Services.Scanner; #nullable enable /// @@ -25,20 +27,20 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser var fileName = directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath); // Mylar often outputs cover.jpg, ignore it by default - if (string.IsNullOrEmpty(fileName) || Parser.IsCoverImage(directoryService.FileSystem.Path.GetFileName(filePath))) return null; + if (string.IsNullOrEmpty(fileName) || Scanner.Parser.IsCoverImage(directoryService.FileSystem.Path.GetFileName(filePath))) return null; var directoryName = directoryService.FileSystem.DirectoryInfo.New(rootPath).Name; var info = new ParserInfo() { Filename = Path.GetFileName(filePath), - Format = Parser.ParseFormat(filePath), - Title = Parser.RemoveExtensionIfSupported(fileName)!, - FullFilePath = Parser.NormalizePath(filePath), + Format = Scanner.Parser.ParseFormat(filePath), + Title = Scanner.Parser.RemoveExtensionIfSupported(fileName)!, + FullFilePath = Scanner.Parser.NormalizePath(filePath), Series = string.Empty, ComicInfo = comicInfo, - Chapters = Parser.ParseChapter(fileName, type), - Volumes = Parser.ParseVolume(fileName, type) + Chapters = Scanner.Parser.ParseChapter(fileName, type), + Volumes = Scanner.Parser.ParseVolume(fileName, type) }; // See if we can formulate the name from the ComicInfo @@ -55,30 +57,30 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser { foreach (var directory in directories) { - if (!Parser.IsSeriesAndYear(directory)) continue; + if (!Scanner.Parser.IsSeriesAndYear(directory)) continue; info.Series = directory; - info.Volumes = Parser.ParseYear(directory); + info.Volumes = Scanner.Parser.ParseYear(directory); break; } // When there was at least one directory and we failed to parse the series, this is the final fallback if (string.IsNullOrEmpty(info.Series)) { - info.Series = Parser.CleanTitle(directories[0], true); + info.Series = Scanner.Parser.CleanTitle(directories[0], true); } } else { - if (Parser.IsSeriesAndYear(directoryName)) + if (Scanner.Parser.IsSeriesAndYear(directoryName)) { info.Series = directoryName; - info.Volumes = Parser.ParseYear(directoryName); + info.Volumes = Scanner.Parser.ParseYear(directoryName); } } } // Check if this is a Special/Annual - info.IsSpecial = Parser.IsSpecial(info.Filename, type) || Parser.IsSpecial(info.ComicInfo?.Format, type); + info.IsSpecial = Scanner.Parser.IsSpecial(info.Filename, type) || Scanner.Parser.IsSpecial(info.ComicInfo?.Format, type); // Patch in other information from ComicInfo if (enableMetadata) @@ -88,7 +90,7 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser if (string.IsNullOrEmpty(info.Series)) { - info.Series = Parser.CleanTitle(directoryName, true); + info.Series = Scanner.Parser.CleanTitle(directoryName, true); } @@ -121,10 +123,10 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser if (!string.IsNullOrEmpty(info.ComicInfo.Number)) { info.Chapters = info.ComicInfo.Number; - if (info.IsSpecial && !Parser.IsDefaultChapter(info.Chapters)) + if (info.IsSpecial && !Scanner.Parser.IsDefaultChapter(info.Chapters)) { info.IsSpecial = false; - info.Volumes = $"{Parser.SpecialVolumeNumber}"; + info.Volumes = $"{Scanner.Parser.SpecialVolumeNumber}"; } } diff --git a/API/Services/Tasks/Scanner/Parser/DefaultParser.cs b/Kavita.Services/Scanner/DefaultParser.cs similarity index 75% rename from API/Services/Tasks/Scanner/Parser/DefaultParser.cs rename to Kavita.Services/Scanner/DefaultParser.cs index 20b48271c..0d3a1c7bd 100644 --- a/API/Services/Tasks/Scanner/Parser/DefaultParser.cs +++ b/Kavita.Services/Scanner/DefaultParser.cs @@ -1,9 +1,10 @@ using System.Linq; -using API.Data.Metadata; -using API.Entities.Enums; +using Kavita.API.Services; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Models.Parser; -namespace API.Services.Tasks.Scanner.Parser; -#nullable enable +namespace Kavita.Services.Scanner; public interface IDefaultParser { @@ -39,17 +40,17 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau public void ParseFromFallbackFolders(string filePath, string rootPath, LibraryType type, ref ParserInfo ret) { var fallbackFolders = directoryService.GetFoldersTillRoot(rootPath, filePath) - .Where(f => !Parser.IsSpecial(f, type)) + .Where(f => !Scanner.Parser.IsSpecial(f, type)) .ToList(); if (fallbackFolders.Count == 0) { var rootFolderName = directoryService.FileSystem.DirectoryInfo.New(rootPath).Name; - var series = Parser.ParseSeries(rootFolderName, type); + var series = Scanner.Parser.ParseSeries(rootFolderName, type); if (string.IsNullOrEmpty(series)) { - ret.Series = Parser.CleanTitle(rootFolderName, type is LibraryType.Comic); + ret.Series = Scanner.Parser.CleanTitle(rootFolderName, type is LibraryType.Comic); return; } @@ -64,18 +65,18 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau { var folder = fallbackFolders[i]; - var parsedVolume = Parser.ParseVolume(folder, type); - var parsedChapter = Parser.ParseChapter(folder, type); + var parsedVolume = Scanner.Parser.ParseVolume(folder, type); + var parsedChapter = Scanner.Parser.ParseChapter(folder, type); - var isLooseLeafVolume = Parser.IsLooseLeafVolume(parsedVolume); - var isDefaultChapter = Parser.IsDefaultChapter(parsedChapter); + var isLooseLeafVolume = Scanner.Parser.IsLooseLeafVolume(parsedVolume); + var isDefaultChapter = Scanner.Parser.IsDefaultChapter(parsedChapter); - if ((string.IsNullOrEmpty(ret.Volumes) || Parser.IsLooseLeafVolume(ret.Volumes)) + if ((string.IsNullOrEmpty(ret.Volumes) || Scanner.Parser.IsLooseLeafVolume(ret.Volumes)) && !string.IsNullOrEmpty(parsedVolume) && !isLooseLeafVolume) { ret.Volumes = parsedVolume; } - if ((string.IsNullOrEmpty(ret.Chapters) || ret.Chapters.Equals(Parser.DefaultChapter)) + if ((string.IsNullOrEmpty(ret.Chapters) || ret.Chapters.Equals(Scanner.Parser.DefaultChapter)) && !string.IsNullOrEmpty(parsedChapter) && !isDefaultChapter) { ret.Chapters = parsedChapter; @@ -84,11 +85,11 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau // Generally users group in series folders. Let's try to parse series from the top folder if (!folder.Equals(ret.Series) && i == fallbackFolders.Count - 1) { - var series = Parser.ParseSeries(folder, type); + var series = Scanner.Parser.ParseSeries(folder, type); if (string.IsNullOrEmpty(series)) { - ret.Series = Parser.CleanTitle(folder, type is LibraryType.Comic); + ret.Series = Scanner.Parser.CleanTitle(folder, type is LibraryType.Comic); break; } @@ -122,11 +123,11 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau info.LocalizedSeries = info.ComicInfo.LocalizedSeries.Trim(); } - if (!string.IsNullOrEmpty(info.ComicInfo.Format) && Parser.HasComicInfoSpecial(info.ComicInfo.Format)) + if (!string.IsNullOrEmpty(info.ComicInfo.Format) && Scanner.Parser.HasComicInfoSpecial(info.ComicInfo.Format)) { info.IsSpecial = true; - info.Chapters = Parser.DefaultChapter; - info.Volumes = Parser.SpecialVolume; + info.Chapters = Scanner.Parser.DefaultChapter; + info.Volumes = Scanner.Parser.SpecialVolume; } // Patch is SeriesSort from ComicInfo @@ -141,7 +142,7 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau protected static bool IsEmptyOrDefault(string volumes, string chapters) { - return (string.IsNullOrEmpty(chapters) || Parser.IsDefaultChapter(chapters)) && - (string.IsNullOrEmpty(volumes) || Parser.IsLooseLeafVolume(volumes)); + return (string.IsNullOrEmpty(chapters) || Scanner.Parser.IsDefaultChapter(chapters)) && + (string.IsNullOrEmpty(volumes) || Scanner.Parser.IsLooseLeafVolume(volumes)); } } diff --git a/API/Services/Tasks/Scanner/Parser/ImageParser.cs b/Kavita.Services/Scanner/ImageParser.cs similarity index 74% rename from API/Services/Tasks/Scanner/Parser/ImageParser.cs rename to Kavita.Services/Scanner/ImageParser.cs index 12f9f4d50..ff92749bf 100644 --- a/API/Services/Tasks/Scanner/Parser/ImageParser.cs +++ b/Kavita.Services/Scanner/ImageParser.cs @@ -1,8 +1,10 @@ using System.IO; -using API.Data.Metadata; -using API.Entities.Enums; +using Kavita.API.Services; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Models.Parser; -namespace API.Services.Tasks.Scanner.Parser; +namespace Kavita.Services.Scanner; #nullable enable public class ImageParser(IDirectoryService directoryService) : DefaultParser(directoryService) @@ -16,12 +18,12 @@ public class ImageParser(IDirectoryService directoryService) : DefaultParser(dir var ret = new ParserInfo { Series = directoryName, - Volumes = Parser.LooseLeafVolume, - Chapters = Parser.DefaultChapter, + Volumes = Scanner.Parser.LooseLeafVolume, + Chapters = Scanner.Parser.DefaultChapter, ComicInfo = comicInfo, Format = MangaFormat.Image, Filename = Path.GetFileName(filePath), - FullFilePath = Parser.NormalizePath(filePath), + FullFilePath = Scanner.Parser.NormalizePath(filePath), Title = fileName, }; ParseFromFallbackFolders(filePath, libraryRoot, LibraryType.Image, ref ret); @@ -29,13 +31,13 @@ public class ImageParser(IDirectoryService directoryService) : DefaultParser(dir if (IsEmptyOrDefault(ret.Volumes, ret.Chapters)) { ret.IsSpecial = true; - ret.Volumes = Parser.SpecialVolume; + ret.Volumes = Scanner.Parser.SpecialVolume; } // Override the series name, as fallback folders needs it to try and parse folder name if (string.IsNullOrEmpty(ret.Series) || ret.Series.Equals(directoryName)) { - ret.Series = Parser.CleanTitle(directoryName); + ret.Series = Scanner.Parser.CleanTitle(directoryName); } @@ -50,6 +52,6 @@ public class ImageParser(IDirectoryService directoryService) : DefaultParser(dir /// public override bool IsApplicable(string filePath, LibraryType type) { - return type == LibraryType.Image && Parser.IsImage(filePath); + return type == LibraryType.Image && Scanner.Parser.IsImage(filePath); } } diff --git a/API/Services/Tasks/Scanner/LibraryWatcher.cs b/Kavita.Services/Scanner/LibraryWatcher.cs similarity index 93% rename from API/Services/Tasks/Scanner/LibraryWatcher.cs rename to Kavita.Services/Scanner/LibraryWatcher.cs index fec0304a8..e8146b134 100644 --- a/API/Services/Tasks/Scanner/LibraryWatcher.cs +++ b/Kavita.Services/Scanner/LibraryWatcher.cs @@ -1,35 +1,18 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.Entities.Enums; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Scanner; +using Kavita.Models.Entities.Enums; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace API.Services.Tasks.Scanner; -#nullable enable - -public interface ILibraryWatcher -{ - /// - /// Start watching all library folders - /// - /// - Task StartWatching(); - /// - /// Stop watching all folders - /// - void StopWatching(); - /// - /// Essentially stops then starts watching. Useful if there is a change in folders or libraries - /// - /// - Task RestartWatching(); -} +namespace Kavita.Services.Scanner; /// /// Responsible for watching the file system and processing change events. This is mainly responsible for invoking @@ -91,7 +74,7 @@ public class LibraryWatcher : ILibraryWatcher .Where(l => l.FolderWatching) .SelectMany(l => l.Folders) .Distinct() - .Select(Parser.Parser.NormalizePath) + .Select(Parser.NormalizePath) .Where(_directoryService.Exists) .ToList(); @@ -254,14 +237,14 @@ public class LibraryWatcher : ILibraryWatcher try { // If the change occurs in a blacklisted folder path, then abort processing - if (Parser.Parser.HasBlacklistedFolderInPath(filePath)) + if (Parser.HasBlacklistedFolderInPath(filePath)) { return; } // If not a directory change AND file is not an archive or book, ignore if (!isDirectoryChange && - !(Parser.Parser.IsArchive(filePath) || Parser.Parser.IsBook(filePath))) + !(Parser.IsArchive(filePath) || Parser.IsBook(filePath))) { _logger.LogTrace("[LibraryWatcher] Change from {FilePath} is not an archive or book, ignoring change", filePath); return; @@ -270,7 +253,7 @@ public class LibraryWatcher : ILibraryWatcher var libraryFolders = (await _unitOfWork.LibraryRepository.GetLibraryDtosAsync()) .SelectMany(l => l.Folders) .Distinct() - .Select(Parser.Parser.NormalizePath) + .Select(Parser.NormalizePath) .Where(_directoryService.Exists) .ToList(); @@ -310,7 +293,7 @@ public class LibraryWatcher : ILibraryWatcher if (rootFolder.Count == 0) return string.Empty; // Select the first folder and join with library folder, this should give us the folder to scan. - return Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(libraryFolder, rootFolder[^1])); + return Parser.NormalizePath(_directoryService.FileSystem.Path.Join(libraryFolder, rootFolder[^1])); } diff --git a/API/Services/Tasks/Scanner/ParseScannedFiles.cs b/Kavita.Services/Scanner/ParseScannedFiles.cs similarity index 92% rename from API/Services/Tasks/Scanner/ParseScannedFiles.cs rename to Kavita.Services/Scanner/ParseScannedFiles.cs index f846831e6..7a70914fc 100644 --- a/API/Services/Tasks/Scanner/ParseScannedFiles.cs +++ b/Kavita.Services/Scanner/ParseScannedFiles.cs @@ -1,93 +1,22 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; +using Kavita.Common.Extensions; using Kavita.Common.Helpers; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Parser; +using Kavita.Services.Extensions; using Microsoft.Extensions.Logging; -namespace API.Services.Tasks.Scanner; -#nullable enable - -public class ParsedSeries -{ - /// - /// Name of the Series - /// - public required string Name { get; init; } - /// - /// Normalized Name of the Series - /// - public required string NormalizedName { get; init; } - /// - /// Format of the Series - /// - public required MangaFormat Format { get; init; } - /// - /// Has this Series changed or not aka do we need to process it or not. - /// - public bool HasChanged { get; set; } -} - -public class ScanResult -{ - /// - /// A list of files in the Folder. Empty if HasChanged = false - /// - public IList Files { get; set; } - /// - /// A nested folder from Library Root (at any level) - /// - public string Folder { get; set; } - /// - /// The library root - /// - public string LibraryRoot { get; set; } - /// - /// Was the Folder scanned or not. If not modified since last scan, this will be false and Files empty - /// - public bool HasChanged { get; set; } - /// - /// Set in Stage 2: Parsed Info from the Files - /// - public IList ParserInfos { get; set; } -} - -/// -/// The final product of ParseScannedFiles. This has all the processed parserInfo and is ready for tracking/processing into entities -/// -public class ScannedSeriesResult -{ - /// - /// Was the Folder scanned or not. If not modified since last scan, this will be false and indicates that upstream should count this as skipped - /// - public bool HasChanged { get; set; } - /// - /// The Parsed Series information used for tracking - /// - public ParsedSeries ParsedSeries { get; set; } - /// - /// Parsed files - /// - public IList ParsedInfos { get; set; } -} - -public class SeriesModified -{ - public required string? FolderPath { get; set; } - public required string? LowestFolderPath { get; set; } - public required string SeriesName { get; set; } - public DateTime LastScanned { get; set; } - public MangaFormat Format { get; set; } - public IEnumerable LibraryRoots { get; set; } = ArraySegment.Empty; -} +namespace Kavita.Services.Scanner; /// /// Responsible for taking parsed info from ReadingItemService and DirectoryService and combining them to emit DB work @@ -152,7 +81,7 @@ public class ParseScannedFiles Library library, bool forceCheck, GlobMatcher matcher, List result, string fileExtensions) { var allDirectories = _directoryService.GetAllDirectories(folderPath, matcher) - .Select(Parser.Parser.NormalizePath) + .Select(Scanner.Parser.NormalizePath) .OrderByDescending(d => d.Length) .ToList(); @@ -318,10 +247,10 @@ public class ParseScannedFiles private async Task> ScanSingleDirectory(string folderPath, IDictionary> seriesPaths, Library library, bool forceCheck, List result, string fileExtensions, GlobMatcher matcher) { - var normalizedPath = Parser.Parser.NormalizePath(folderPath); + var normalizedPath = Scanner.Parser.NormalizePath(folderPath); var libraryRoot = library.Folders.FirstOrDefault(f => - normalizedPath.Contains(Parser.Parser.NormalizePath(f.Path)))?.Path ?? + normalizedPath.Contains(Scanner.Parser.NormalizePath(f.Path)))?.Path ?? folderPath; await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, @@ -357,7 +286,7 @@ public class ParseScannedFiles return new ScanResult() { Files = files, - Folder = Parser.Parser.NormalizePath(folderPath), + Folder = Scanner.Parser.NormalizePath(folderPath), LibraryRoot = libraryRoot, HasChanged = hasChanged }; @@ -708,7 +637,7 @@ public class ParseScannedFiles case 1: return seriesForLocalized[0]; case <= 2: - return seriesForLocalized.FirstOrDefault(s => !s.Equals(Parser.Parser.Normalize(localizedSeries))); + return seriesForLocalized.FirstOrDefault(s => !s.Equals(Scanner.Parser.Normalize(localizedSeries))); default: _logger.LogError( "[ScannerService] Multiple series detected across scan results that contain localized series. " + @@ -763,7 +692,7 @@ public class ParseScannedFiles /// private async Task ParseFiles(ScanResult result, IDictionary> seriesPaths, Library library) { - var normalizedFolder = Parser.Parser.NormalizePath(result.Folder); + var normalizedFolder = Scanner.Parser.NormalizePath(result.Folder); // If folder hasn't changed, generate fake ParserInfos if (!result.HasChanged) @@ -849,7 +778,7 @@ public class ParseScannedFiles if (specialTreatment) { chapters = infos - .OrderByNatural(info => Parser.Parser.RemoveExtensionIfSupported(info.Filename)!) + .OrderByNatural(info => Scanner.Parser.RemoveExtensionIfSupported(info.Filename)!) .ToList(); foreach (var chapter in chapters) @@ -871,7 +800,7 @@ public class ParseScannedFiles { // Use MinNumber in case there is a range, as otherwise sort order will cause it to be processed last var chapterNum = - $"{Parser.Parser.MinNumberFromRange(chapter.Chapters).ToString(CultureInfo.InvariantCulture)}"; + $"{Scanner.Parser.MinNumberFromRange(chapter.Chapters).ToString(CultureInfo.InvariantCulture)}"; if (float.TryParse(chapterNum, NumberStyles.Any, CultureInfo.InvariantCulture, out var parsedChapter)) { // Parsed successfully, use the numeric value diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/Kavita.Services/Scanner/Parser.cs similarity index 98% rename from API/Services/Tasks/Scanner/Parser/Parser.cs rename to Kavita.Services/Scanner/Parser.cs index f0b16c8d1..5444aac28 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/Kavita.Services/Scanner/Parser.cs @@ -1,26 +1,28 @@ using System; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text.RegularExpressions; -using API.Entities.Enums; -using API.Extensions; +using Kavita.Common.Constants; +using Kavita.Common.Extensions; +using Kavita.Models.Constants; +using Kavita.Models.Entities.Enums; -namespace API.Services.Tasks.Scanner.Parser; -#nullable enable +namespace Kavita.Services.Scanner; public static partial class Parser { // NOTE: If you change this, don't forget to change in the UI (see Series Detail) - public const string DefaultChapter = "-100000"; - public const string LooseLeafVolume = "-100000"; - public const int DefaultChapterNumber = -100_000; - public const int LooseLeafVolumeNumber = -100_000; + public const string DefaultChapter = ParserConstants.DefaultChapter; + public const string LooseLeafVolume = ParserConstants.LooseLeafVolume; + public const int DefaultChapterNumber = ParserConstants.DefaultChapterNumber; + public const int LooseLeafVolumeNumber = ParserConstants.LooseLeafVolumeNumber; /// /// The Volume Number of Specials to reside in /// - public const int SpecialVolumeNumber = 100_000; - public const string SpecialVolume = "100000"; + public const int SpecialVolumeNumber = ParserConstants.SpecialVolumeNumber; + public const string SpecialVolume = ParserConstants.SpecialVolume; public static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(500); @@ -105,13 +107,6 @@ public static partial class Parser private static readonly Regex CoverImageRegex = new(@"(? - /// Normalize everything within Kavita. Some characters don't fall under Unicode, like full-width characters and need to be - /// added on a case-by-case basis. - /// - private static readonly Regex NormalizeRegex = new(@"[^\p{L}0-9\+!*!+]", - MatchOptions, RegexTimeout); - /// /// Supports Batman (2020) or Batman (2) /// @@ -1076,7 +1071,7 @@ public static partial class Parser public static string Normalize(string name) { - return NormalizeRegex.Replace(name, string.Empty).Trim().ToLower(); + return name.ToNormalized(); } /// @@ -1160,8 +1155,7 @@ public static partial class Parser /// public static string NormalizePath(string? path) { - return string.IsNullOrEmpty(path) ? string.Empty : path.Replace('\\', Path.AltDirectorySeparatorChar) - .Replace(@"//", Path.AltDirectorySeparatorChar + string.Empty); + return path.NormalizePath(); } /// @@ -1219,6 +1213,7 @@ public static partial class Parser return match.Groups["Year"].Value; } + [return: NotNullIfNotNull(nameof(filename))] public static string? RemoveExtensionIfSupported(string? filename) { if (string.IsNullOrEmpty(filename)) return filename; diff --git a/API/Services/Tasks/Scanner/Parser/PdfParser.cs b/Kavita.Services/Scanner/PdfParser.cs similarity index 66% rename from API/Services/Tasks/Scanner/Parser/PdfParser.cs rename to Kavita.Services/Scanner/PdfParser.cs index 1e43f3bb4..4bde04bd5 100644 --- a/API/Services/Tasks/Scanner/Parser/PdfParser.cs +++ b/Kavita.Services/Scanner/PdfParser.cs @@ -1,8 +1,10 @@ using System.IO; -using API.Data.Metadata; -using API.Entities.Enums; +using Kavita.API.Services; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Metadata; +using Kavita.Models.Parser; -namespace API.Services.Tasks.Scanner.Parser; +namespace Kavita.Services.Scanner; public class PdfParser(IDirectoryService directoryService) : DefaultParser(directoryService) { @@ -12,21 +14,21 @@ public class PdfParser(IDirectoryService directoryService) : DefaultParser(direc var ret = new ParserInfo { Filename = Path.GetFileName(filePath), - Format = Parser.ParseFormat(filePath), - Title = Parser.RemoveExtensionIfSupported(fileName)!, - FullFilePath = Parser.NormalizePath(filePath), + Format = Scanner.Parser.ParseFormat(filePath), + Title = Scanner.Parser.RemoveExtensionIfSupported(fileName)!, + FullFilePath = Scanner.Parser.NormalizePath(filePath), Series = string.Empty, ComicInfo = comicInfo, - Chapters = Parser.ParseChapter(fileName, type) + Chapters = Scanner.Parser.ParseChapter(fileName, type) }; if (type == LibraryType.Book) { - ret.Chapters = Parser.DefaultChapter; + ret.Chapters = Scanner.Parser.DefaultChapter; } - ret.Series = Parser.ParseSeries(fileName, type); - ret.Volumes = Parser.ParseVolume(fileName, type); + ret.Series = Scanner.Parser.ParseSeries(fileName, type); + ret.Volumes = Scanner.Parser.ParseVolume(fileName, type); if (ret.Series == string.Empty) { @@ -34,17 +36,17 @@ public class PdfParser(IDirectoryService directoryService) : DefaultParser(direc ParseFromFallbackFolders(filePath, rootPath, type, ref ret); } - var edition = Parser.ParseEdition(fileName); + var edition = Scanner.Parser.ParseEdition(fileName); if (!string.IsNullOrEmpty(edition)) { - ret.Series = Parser.CleanTitle(ret.Series.Replace(edition, string.Empty), type is LibraryType.Comic); + ret.Series = Scanner.Parser.CleanTitle(ret.Series.Replace(edition, string.Empty), type is LibraryType.Comic); ret.Edition = edition; } - var isSpecial = Parser.IsSpecial(fileName, type); + var isSpecial = Scanner.Parser.IsSpecial(fileName, type); // We must ensure that we can only parse a special out. As some files will have v20 c171-180+Omake and that // could cause a problem as Omake is a special term, but there is valid volume/chapter information. - if (Parser.IsDefaultChapter(ret.Chapters) && Parser.IsLooseLeafVolume(ret.Volumes) && isSpecial) + if (Scanner.Parser.IsDefaultChapter(ret.Chapters) && Scanner.Parser.IsLooseLeafVolume(ret.Volumes) && isSpecial) { ret.IsSpecial = true; // NOTE: This can cause some complications, we should try to be a bit less aggressive to fallback to folder @@ -52,12 +54,12 @@ public class PdfParser(IDirectoryService directoryService) : DefaultParser(direc } // If we are a special with marker, we need to ensure we use the correct series name. we can do this by falling back to Folder name - if (Parser.HasSpecialMarker(fileName)) + if (Scanner.Parser.HasSpecialMarker(fileName)) { ret.IsSpecial = true; - ret.SpecialIndex = Parser.ParseSpecialIndex(fileName); - ret.Chapters = Parser.DefaultChapter; - ret.Volumes = Parser.SpecialVolume; + ret.SpecialIndex = Scanner.Parser.ParseSpecialIndex(fileName); + ret.Chapters = Scanner.Parser.DefaultChapter; + ret.Volumes = Scanner.Parser.SpecialVolume; var tempRootPath = rootPath; if (rootPath.EndsWith("Specials") || rootPath.EndsWith("Specials/")) @@ -80,11 +82,11 @@ public class PdfParser(IDirectoryService directoryService) : DefaultParser(direc } - if (Parser.IsDefaultChapter(ret.Chapters) && Parser.IsLooseLeafVolume(ret.Volumes) && type == LibraryType.Book) + if (Scanner.Parser.IsDefaultChapter(ret.Chapters) && Scanner.Parser.IsLooseLeafVolume(ret.Volumes) && type == LibraryType.Book) { ret.IsSpecial = true; - ret.Chapters = Parser.DefaultChapter; - ret.Volumes = Parser.SpecialVolume; + ret.Chapters = Scanner.Parser.DefaultChapter; + ret.Volumes = Scanner.Parser.SpecialVolume; ParseFromFallbackFolders(filePath, rootPath, type, ref ret); } @@ -103,11 +105,11 @@ public class PdfParser(IDirectoryService directoryService) : DefaultParser(direc if (string.IsNullOrEmpty(ret.Series)) { - ret.Series = Parser.CleanTitle(fileName, type is LibraryType.Comic); + ret.Series = Scanner.Parser.CleanTitle(fileName, type is LibraryType.Comic); } // Pdfs may have .pdf in the series name, remove that - if (Parser.IsPdf(filePath) && ret.Series.ToLower().EndsWith(".pdf")) + if (Scanner.Parser.IsPdf(filePath) && ret.Series.ToLower().EndsWith(".pdf")) { ret.Series = ret.Series.Substring(0, ret.Series.Length - ".pdf".Length); } @@ -115,7 +117,7 @@ public class PdfParser(IDirectoryService directoryService) : DefaultParser(direc // v0.8.x: Introducing a change where Specials will go in a separate Volume with a reserved number if (ret.IsSpecial) { - ret.Volumes = $"{Parser.SpecialVolumeNumber}"; + ret.Volumes = $"{Scanner.Parser.SpecialVolumeNumber}"; } return string.IsNullOrEmpty(ret.Series) ? null : ret; @@ -129,6 +131,6 @@ public class PdfParser(IDirectoryService directoryService) : DefaultParser(direc /// public override bool IsApplicable(string filePath, LibraryType type) { - return Parser.IsPdf(filePath); + return Scanner.Parser.IsPdf(filePath); } } diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/Kavita.Services/Scanner/ProcessSeries.cs similarity index 94% rename from API/Services/Tasks/Scanner/ProcessSeries.cs rename to Kavita.Services/Scanner/ProcessSeries.cs index 390071e95..4bb6790da 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/Kavita.Services/Scanner/ProcessSeries.cs @@ -1,42 +1,36 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Data; -using API.Data.Metadata; -using API.Data.Repositories; -using API.DTOs.KavitaPlus.Metadata; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Person; -using API.Extensions; -using API.Helpers; -using API.Helpers.Builders; -using API.Services.Plus; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Helpers; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +using Kavita.API.Services.Scanner; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Person; +using Kavita.Models.Metadata; +using Kavita.Models.Parser; +using Kavita.Services.Builders; +using Kavita.Services.Extensions; +using Kavita.Services.Helpers; +using Kavita.Services.Plus; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Services.Tasks.Scanner; -#nullable enable - -public interface IProcessSeries -{ - Task ProcessSeriesAsync(MetadataSettingsDto settings, IList parsedInfos, ProcessSeriesArgs args); -} - -public sealed record ProcessSeriesArgs -{ - public required Library Library { get; init; } - public required int TotalToProcess { get; init; } - public required int LeftToProcess { get; init; } - public bool ForceUpdate { get; init; } = false; -} +namespace Kavita.Services.Scanner; internal sealed record UpdateChapterArgs { @@ -249,7 +243,7 @@ public class ProcessSeries( 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)}"; + $"Filename: {Parser.NormalizePath(firstCollision.FolderPath)}Filename: {Parser.NormalizePath(secondCollision.FolderPath)}"; var htmlTable = $"{string.Join(string.Empty, tableRows)}
Series 1Series 2
"; @@ -265,8 +259,8 @@ public class ProcessSeries( private async Task UpdateSeriesFolderPath(IEnumerable parsedInfos, Library library, Series series) { - var libraryFolders = library.Folders.Select(l => Parser.Parser.NormalizePath(l.Path)).ToList(); - var seriesFiles = parsedInfos.Select(f => Parser.Parser.NormalizePath(f.FullFilePath)).ToList(); + var libraryFolders = library.Folders.Select(l => Parser.NormalizePath(l.Path)).ToList(); + var seriesFiles = parsedInfos.Select(f => Parser.NormalizePath(f.FullFilePath)).ToList(); var seriesDirs = directoryService.FindHighestDirectoriesFromFiles(libraryFolders, seriesFiles); if (seriesDirs.Keys.Count == 0) { @@ -283,7 +277,7 @@ public class ProcessSeries( { // BUG: FolderPath can be a level higher than it needs to be. I'm not sure why it's like this, but I thought it should be one level lower. // I think it's like this because higher level is checked or not checked. But i think we can do both - series.FolderPath = Parser.Parser.NormalizePath(seriesDirs.Keys.First()); + series.FolderPath = Parser.NormalizePath(seriesDirs.Keys.First()); logger.LogDebug("Updating {Series} FolderPath to {FolderPath}", series.Name, series.FolderPath); } } @@ -527,7 +521,7 @@ public class ProcessSeries( series.Metadata.MaxCount = chapters.Max(chapter => chapter.Count); var nonSpecialVolumes = series.Volumes - .Where(v => v.MaxNumber.IsNot(Parser.Parser.SpecialVolumeNumber)) + .Where(v => v.MaxNumber.IsNot(Parser.SpecialVolumeNumber)) .ToList(); var maxVolume = (int)(nonSpecialVolumes.Any() ? nonSpecialVolumes.Max(v => v.MaxNumber) : 0); @@ -543,7 +537,7 @@ public class ProcessSeries( // If a series has a TotalCount of 1 (or no total count) and there is only a Special, mark it as Complete series.Metadata.MaxCount = series.Metadata.TotalCount; } - else if ((maxChapter == Parser.Parser.DefaultChapterNumber || maxChapter > series.Metadata.TotalCount) && + else if ((maxChapter == Parser.DefaultChapterNumber || maxChapter > series.Metadata.TotalCount) && maxVolume <= series.Metadata.TotalCount) { series.Metadata.MaxCount = maxVolume; @@ -690,9 +684,9 @@ public class ProcessSeries( // Add files AddOrUpdateFileForChapter(chapter, info, args.ForceUpdate); - chapter.Number = Parser.Parser.MinNumberFromRange(info.Chapters).ToString(CultureInfo.InvariantCulture); - chapter.MinNumber = Parser.Parser.MinNumberFromRange(info.Chapters); - chapter.MaxNumber = Parser.Parser.MaxNumberFromRange(info.Chapters); + chapter.Number = Parser.MinNumberFromRange(info.Chapters).ToString(CultureInfo.InvariantCulture); + chapter.MinNumber = Parser.MinNumberFromRange(info.Chapters); + chapter.MaxNumber = Parser.MaxNumberFromRange(info.Chapters); chapter.Range = chapter.GetNumberTitle(); if (!chapter.SortOrderLocked) @@ -752,7 +746,7 @@ public class ProcessSeries( if (hasMatchingDirectory) { existingChapter.Files = existingChapter.Files - .Where(f => parsedInfos.Any(p => Parser.Parser.NormalizePath(p.FullFilePath) == Parser.Parser.NormalizePath(f.FilePath))) + .Where(f => parsedInfos.Any(p => Parser.NormalizePath(p.FullFilePath) == Parser.NormalizePath(f.FilePath))) .OrderByNatural(f => f.FilePath) .ToList(); @@ -796,8 +790,8 @@ public class ProcessSeries( existingFile.Pages = readingItemService.GetNumberOfPages(info.FullFilePath, info.Format); existingFile.Extension = fileInfo.Extension.ToLowerInvariant(); - existingFile.FileName = Parser.Parser.RemoveExtensionIfSupported(existingFile.FilePath); - existingFile.FilePath = Parser.Parser.NormalizePath(existingFile.FilePath); + existingFile.FileName = Parser.RemoveExtensionIfSupported(existingFile.FilePath); + existingFile.FilePath = Parser.NormalizePath(existingFile.FilePath); existingFile.Bytes = fileInfo.Length; existingFile.KoreaderHash = KoreaderHelper.HashContents(existingFile.FilePath); @@ -878,11 +872,11 @@ public class ProcessSeries( if (!string.IsNullOrEmpty(comicInfo.Web)) { - chapter.WebLinks = string.Join(",", comicInfo.Web - .Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) - ); + chapter.WebLinks = string.Join(",", comicInfo.Web.SplitBy(',')); // TODO: For each weblink, try to parse out some MetadataIds and store in the Chapter directly for matching (CBL) + // var aniListId = ScrobblingHelper.GetAniListId(chapter.WebLinks); + // var malId = ScrobblingHelper.GetMalId(chapter.WebLinks); } if (!chapter.ISBNLocked && !string.IsNullOrEmpty(comicInfo.Isbn)) @@ -916,8 +910,8 @@ public class ProcessSeries( if (!chapter.GenresLocked || !chapter.TagsLocked) { - var genres = TagHelper.GetTagValues(comicInfo.Genre); - var tags = TagHelper.GetTagValues(comicInfo.Tags); + var genres = comicInfo.Genre.SplitBy(','); + var tags = comicInfo.Tags.SplitBy(','); ExternalMetadataService.GenerateExternalGenreAndTagsList(genres, tags, args.Settings, out var finalTags, out var finalGenres); diff --git a/API/Services/Tasks/ScannerService.cs b/Kavita.Services/Scanner/ScannerService.cs similarity index 65% rename from API/Services/Tasks/ScannerService.cs rename to Kavita.Services/Scanner/ScannerService.cs index d633da92d..224ec1406 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/Kavita.Services/Scanner/ScannerService.cs @@ -6,53 +6,26 @@ using System.IO; using System.Linq; using System.Threading.Channels; using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Settings; -using API.Entities; -using API.Extensions; -using API.Helpers; -using API.Helpers.Builders; -using API.Services.Plus; -using API.Services.Tasks.Metadata; -using API.Services.Tasks.Scanner; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Metadata; +using Kavita.API.Services.Scanner; +using Kavita.API.Services.SignalR; +using Kavita.Common.Extensions; +using Kavita.Models.Builders; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Parser; +using Kavita.Services.Helpers; +using Kavita.Services.Plus; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace API.Services.Tasks; -#nullable enable - -public interface IScannerService -{ - /// - /// Given a library id, scans folders for said library. Parses files and generates DB updates. Will overwrite - /// cover images if forceUpdate is true. - /// - /// Library to scan against - /// Don't perform optimization checks, defaults to false - [Queue(TaskScheduler.ScanQueue)] - [DisableConcurrentExecution(60 * 60 * 60)] - [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - Task ScanLibrary(int libraryId, bool forceUpdate = false, bool isSingleScan = true); - - [Queue(TaskScheduler.ScanQueue)] - [DisableConcurrentExecution(60 * 60 * 60)] - [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - Task ScanLibraries(bool forceUpdate = false); - - [Queue(TaskScheduler.ScanQueue)] - [DisableConcurrentExecution(60 * 60 * 60)] - [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - Task ScanSeries(int seriesId, bool bypassFolderOptimizationChecks = true); - - Task ScanFolder(string folder, string originalPath, bool abortOnNoSeriesMatch = false); - Task AnalyzeFiles(); - -} +namespace Kavita.Services.Scanner; public enum ScanCancelReason { @@ -77,46 +50,31 @@ public enum ScanCancelReason /** * Responsible for Scanning the disk and importing/updating/deleting files -> DB entities. */ -public class ScannerService : IScannerService +public class ScannerService( + IUnitOfWork unitOfWork, + ILogger logger, + IMetadataService metadataService, + ICacheService cacheService, + IEventHub eventHub, + IDirectoryService directoryService, + IReadingItemService readingItemService, + IServiceScopeFactory scopeFactory, + IWordCountAnalyzerService wordCountAnalyzerService) + : IScannerService { public const string Name = "ScannerService"; private const int Timeout = 60 * 60 * 60; // 2.5 days - private readonly IUnitOfWork _unitOfWork; - private readonly ILogger _logger; - private readonly IMetadataService _metadataService; - private readonly ICacheService _cacheService; - private readonly IEventHub _eventHub; - private readonly IDirectoryService _directoryService; - private readonly IReadingItemService _readingItemService; - private readonly IWordCountAnalyzerService _wordCountAnalyzerService; - private readonly IServiceScopeFactory _scopeFactory; - - public ScannerService(IUnitOfWork unitOfWork, ILogger logger, - IMetadataService metadataService, ICacheService cacheService, IEventHub eventHub, - IDirectoryService directoryService, IReadingItemService readingItemService, - IServiceScopeFactory scopeFactory, IWordCountAnalyzerService wordCountAnalyzerService) - { - _unitOfWork = unitOfWork; - _logger = logger; - _metadataService = metadataService; - _cacheService = cacheService; - _eventHub = eventHub; - _directoryService = directoryService; - _readingItemService = readingItemService; - _scopeFactory = scopeFactory; - _wordCountAnalyzerService = wordCountAnalyzerService; - } /// /// This is only used for v0.7 to get files analyzed /// public async Task AnalyzeFiles() { - _logger.LogInformation("Starting Analyze Files task"); - var missingExtensions = await _unitOfWork.MangaFileRepository.GetAllWithMissingExtension(); + logger.LogInformation("Starting Analyze Files task"); + var missingExtensions = await unitOfWork.MangaFileRepository.GetAllWithMissingExtension(); if (missingExtensions.Count == 0) { - _logger.LogInformation("Nothing to do"); + logger.LogInformation("Nothing to do"); return; } @@ -124,16 +82,16 @@ public class ScannerService : IScannerService foreach (var file in missingExtensions) { - var fileInfo = _directoryService.FileSystem.FileInfo.New(file.FilePath); + var fileInfo = directoryService.FileSystem.FileInfo.New(file.FilePath); if (!fileInfo.Exists)continue; file.Extension = fileInfo.Extension.ToLowerInvariant(); file.Bytes = fileInfo.Length; - _unitOfWork.MangaFileRepository.Update(file); + unitOfWork.MangaFileRepository.Update(file); } - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); - _logger.LogInformation("Completed Analyze Files task in {ElapsedTime}", sw.Elapsed); + logger.LogInformation("Completed Analyze Files task in {ElapsedTime}", sw.Elapsed); } /// @@ -148,16 +106,16 @@ public class ScannerService : IScannerService Series? series = null; try { - series = await _unitOfWork.SeriesRepository.GetSeriesThatContainsLowestFolderPath(originalPath, + series = await unitOfWork.SeriesRepository.GetSeriesThatContainsLowestFolderPath(originalPath, SeriesIncludes.Library) ?? - await _unitOfWork.SeriesRepository.GetSeriesByFolderPath(originalPath, SeriesIncludes.Library) ?? - await _unitOfWork.SeriesRepository.GetSeriesByFolderPath(folder, SeriesIncludes.Library); + await unitOfWork.SeriesRepository.GetSeriesByFolderPath(originalPath, SeriesIncludes.Library) ?? + await unitOfWork.SeriesRepository.GetSeriesByFolderPath(folder, SeriesIncludes.Library); } catch (InvalidOperationException ex) { if (ex.Message.Equals("Sequence contains more than one element.")) { - _logger.LogCritical(ex, "[ScannerService] Multiple series map to this folder or folder is at library root. Library scan will be used for ScanFolder"); + logger.LogCritical(ex, "[ScannerService] Multiple series map to this folder or folder is at library root. Library scan will be used for ScanFolder"); } } @@ -165,11 +123,11 @@ public class ScannerService : IScannerService { if (TaskScheduler.HasScanTaskRunningForSeries(series.Id)) { - _logger.LogTrace("[ScannerService] Scan folder invoked for {Folder} but a task is already queued for this series. Dropping request", folder); + logger.LogTrace("[ScannerService] Scan folder invoked for {Folder} but a task is already queued for this series. Dropping request", folder); return; } - _logger.LogInformation("[ScannerService] Scan folder invoked for {Folder}, Series matched to folder and ScanSeries enqueued for 1 minute", folder); + logger.LogInformation("[ScannerService] Scan folder invoked for {Folder}, Series matched to folder and ScanSeries enqueued for 1 minute", folder); BackgroundJob.Schedule(() => ScanSeries(series.Id, false), TimeSpan.FromMinutes(1)); return; } @@ -178,12 +136,12 @@ public class ScannerService : IScannerService // This is basically rework of what's already done in Library Watcher but is needed if invoked via API - var parentDirectory = _directoryService.GetParentDirectoryName(folder); + var parentDirectory = directoryService.GetParentDirectoryName(folder); if (string.IsNullOrEmpty(parentDirectory)) return; - var libraries = (await _unitOfWork.LibraryRepository.GetLibraryDtosAsync()).ToList(); + var libraries = (await unitOfWork.LibraryRepository.GetLibraryDtosAsync()).ToList(); var libraryFolders = libraries.SelectMany(l => l.Folders); - var libraryFolder = libraryFolders.Select(Parser.NormalizePath).FirstOrDefault(f => f.Contains(parentDirectory)); + var libraryFolder = libraryFolders.Select(Parser.Normalize).FirstOrDefault(f => f.Contains(parentDirectory)); if (string.IsNullOrEmpty(libraryFolder)) return; var library = libraries.Find(l => l.Folders.Select(Parser.NormalizePath).Contains(libraryFolder)); @@ -192,7 +150,7 @@ public class ScannerService : IScannerService { if (TaskScheduler.HasScanTaskRunningForLibrary(library.Id)) { - _logger.LogTrace("[ScannerService] Scan folder invoked for {Folder} but a task is already queued for this library. Dropping request", folder); + logger.LogTrace("[ScannerService] Scan folder invoked for {Folder} but a task is already queued for this library. Dropping request", folder); return; } BackgroundJob.Schedule(() => ScanLibrary(library.Id, false, true), TimeSpan.FromMinutes(1)); @@ -211,44 +169,44 @@ public class ScannerService : IScannerService { if (TaskScheduler.HasAlreadyEnqueuedTask(Name, "ScanSeries", [seriesId, bypassFolderOptimizationChecks], TaskScheduler.ScanQueue)) { - _logger.LogInformation("[ScannerService] Scan series invoked but a task is already running/enqueued. Dropping request"); + logger.LogInformation("[ScannerService] Scan series invoked but a task is already running/enqueued. Dropping request"); return; } var sw = Stopwatch.StartNew(); - var series = await _unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(seriesId); + var series = await unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(seriesId); if (series == null) return; // This can occur when UI deletes a series but doesn't update and user re-requests update - var settings = await _unitOfWork.SettingsRepository.GetMetadataSettingDto(); - var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var settings = await unitOfWork.SettingsRepository.GetMetadataSettingDto(); + var serverSettings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - var existingChapterIdsToClean = await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new[] {seriesId}); + var existingChapterIdsToClean = await unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new[] {seriesId}); - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId, LibraryIncludes.Folders | LibraryIncludes.FileTypes | LibraryIncludes.ExcludePatterns); + var library = await unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId, LibraryIncludes.Folders | LibraryIncludes.FileTypes | LibraryIncludes.ExcludePatterns); if (library == null) return; var libraryPaths = library.Folders.Select(f => f.Path).ToList(); if (await ShouldScanSeries(seriesId, library, libraryPaths, series, true) != ScanCancelReason.NoCancel) { - BackgroundJob.Enqueue(() => _metadataService.GenerateCoversForSeries(serverSettings, series.LibraryId, seriesId, false, false)); - BackgroundJob.Enqueue(() => _wordCountAnalyzerService.ScanSeries(library.Id, seriesId, bypassFolderOptimizationChecks)); + BackgroundJob.Enqueue(() => metadataService.GenerateCoversForSeries(serverSettings, series.LibraryId, seriesId, false, false)); + BackgroundJob.Enqueue(() => wordCountAnalyzerService.ScanSeries(library.Id, seriesId, bypassFolderOptimizationChecks)); return; } // TODO: We need to refactor this to handle the path changes better var folderPath = series.LowestFolderPath ?? series.FolderPath; - if (string.IsNullOrEmpty(folderPath) || !_directoryService.Exists(folderPath)) + if (string.IsNullOrEmpty(folderPath) || !directoryService.Exists(folderPath)) { // We don't care if it's multiple due to new scan loop enforcing all in one root directory - var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId); - var seriesDirs = _directoryService.FindHighestDirectoriesFromFiles(libraryPaths, + var files = await unitOfWork.SeriesRepository.GetFilesForSeries(seriesId); + var seriesDirs = directoryService.FindHighestDirectoriesFromFiles(libraryPaths, files.Select(f => f.FilePath).ToList()); if (seriesDirs.Keys.Count == 0) { - _logger.LogCritical("Scan Series has files spread outside a main series folder. Defaulting to library folder (this is expensive)"); - await _eventHub.SendMessageAsync(MessageFactory.Info, MessageFactory.InfoEvent($"{series.Name} is not organized well and scan series will be expensive!", "Scan Series has files spread outside a main series folder. Defaulting to library folder (this is expensive)")); - seriesDirs = _directoryService.FindHighestDirectoriesFromFiles(libraryPaths, files.Select(f => f.FilePath).ToList()); + logger.LogCritical("Scan Series has files spread outside a main series folder. Defaulting to library folder (this is expensive)"); + await eventHub.SendMessageAsync(MessageFactory.Info, MessageFactory.InfoEvent($"{series.Name} is not organized well and scan series will be expensive!", "Scan Series has files spread outside a main series folder. Defaulting to library folder (this is expensive)")); + seriesDirs = directoryService.FindHighestDirectoriesFromFiles(libraryPaths, files.Select(f => f.FilePath).ToList()); } folderPath = seriesDirs.Keys.FirstOrDefault(); @@ -256,27 +214,27 @@ public class ScannerService : IScannerService // We should check if folderPath is a library folder path and if so, return early and tell user to correct their setup. if (!string.IsNullOrEmpty(folderPath) && libraryPaths.Contains(folderPath)) { - _logger.LogCritical("[ScannerSeries] {SeriesName} scan aborted. Files for series are not in a nested folder under library path. Correct this and rescan", series.Name); - await _eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent($"{series.Name} scan aborted", "Files for series are not in a nested folder under library path. Correct this and rescan.")); + logger.LogCritical("[ScannerSeries] {SeriesName} scan aborted. Files for series are not in a nested folder under library path. Correct this and rescan", series.Name); + await eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent($"{series.Name} scan aborted", "Files for series are not in a nested folder under library path. Correct this and rescan.")); return; } } if (string.IsNullOrEmpty(folderPath)) { - _logger.LogCritical("[ScannerSeries] Scan Series could not find a single, valid folder root for files"); - await _eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent($"{series.Name} scan aborted", "Scan Series could not find a single, valid folder root for files")); + logger.LogCritical("[ScannerSeries] Scan Series could not find a single, valid folder root for files"); + await eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent($"{series.Name} scan aborted", "Scan Series could not find a single, valid folder root for files")); return; } - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Started, series.Name, 1)); - _logger.LogInformation("Beginning file scan on {SeriesName}", series.Name); + logger.LogInformation("Beginning file scan on {SeriesName}", series.Name); var (scanElapsedTime, parsedSeries) = await ScanFiles(library, [folderPath], false, true); - _logger.LogInformation("ScanFiles for {Series} took {Time} milliseconds", series.Name, scanElapsedTime); + logger.LogInformation("ScanFiles for {Series} took {Time} milliseconds", series.Name, scanElapsedTime); // Remove any parsedSeries keys that don't belong to our series. This can occur when users store 2 series in the same folder RemoveParsedInfosNotForSeries(parsedSeries, series); @@ -284,31 +242,31 @@ public class ScannerService : IScannerService // If nothing was found, first validate any of the files still exist. If they don't then we have a deletion and can skip the rest of the logic flow if (parsedSeries.Count == 0) { - var seriesFiles = (await _unitOfWork.SeriesRepository.GetFilesForSeries(series.Id)); + var seriesFiles = (await unitOfWork.SeriesRepository.GetFilesForSeries(series.Id)); if (!string.IsNullOrEmpty(series.FolderPath) && !seriesFiles.Where(f => f.FilePath.Contains(series.FolderPath)).Any(m => File.Exists(m.FilePath))) { try { - _unitOfWork.SeriesRepository.Remove(series); + unitOfWork.SeriesRepository.Remove(series); await CommitAndSend(1, sw, scanElapsedTime, series); - await _eventHub.SendMessageAsync(MessageFactory.SeriesRemoved, + await eventHub.SendMessageAsync(MessageFactory.SeriesRemoved, MessageFactory.SeriesRemovedEvent(seriesId, string.Empty, series.LibraryId), false); } catch (Exception ex) { - _logger.LogCritical(ex, "There was an error during ScanSeries to delete the series as no files could be found. Aborting scan"); - await _unitOfWork.RollbackAsync(); + logger.LogCritical(ex, "There was an error during ScanSeries to delete the series as no files could be found. Aborting scan"); + await unitOfWork.RollbackAsync(); return; } } else { // I think we should just fail and tell user to fix their setup. This is extremely expensive for an edge case - _logger.LogCritical("We weren't able to find any files in the series scan, but there should be. Please correct your naming convention or put Series in a dedicated folder. Aborting scan"); - await _eventHub.SendMessageAsync(MessageFactory.Error, + logger.LogCritical("We weren't able to find any files in the series scan, but there should be. Please correct your naming convention or put Series in a dedicated folder. Aborting scan"); + await eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent($"Error scanning {series.Name}", "We weren't able to find any files in the series scan, but there should be. Please correct your naming convention or put Series in a dedicated folder. Aborting scan")); - await _unitOfWork.RollbackAsync(); + await unitOfWork.RollbackAsync(); return; } } @@ -328,7 +286,7 @@ public class ScannerService : IScannerService { current++; - using var scope = _scopeFactory.CreateScope(); + using var scope = scopeFactory.CreateScope(); var processSeries = scope.ServiceProvider.GetRequiredService(); var unitOfWork = scope.ServiceProvider.GetRequiredService(); @@ -353,13 +311,13 @@ public class ScannerService : IScannerService } // Tell UI that this series is done - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Ended, series.Name)); - await _metadataService.RemoveAbandonedMetadataKeys(); + await metadataService.RemoveAbandonedMetadataKeys(); - BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(existingChapterIdsToClean)); - BackgroundJob.Enqueue(() => _directoryService.ClearDirectory(_directoryService.CacheDirectory)); + BackgroundJob.Enqueue(() => cacheService.CleanupChapters(existingChapterIdsToClean)); + BackgroundJob.Enqueue(() => directoryService.ClearDirectory(directoryService.CacheDirectory)); } private static Dictionary> TrackFoundSeriesAndFiles(IList seenSeries) @@ -386,22 +344,22 @@ public class ScannerService : IScannerService private async Task ShouldScanSeries(int seriesId, Library library, IList libraryPaths, Series series, bool bypassFolderChecks = false) { - var seriesFolderPaths = (await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId)) - .Select(f => _directoryService.FileSystem.FileInfo.New(f.FilePath).Directory?.FullName ?? string.Empty) + var seriesFolderPaths = (await unitOfWork.SeriesRepository.GetFilesForSeries(seriesId)) + .Select(f => directoryService.FileSystem.FileInfo.New(f.FilePath).Directory?.FullName ?? string.Empty) .Where(f => !string.IsNullOrEmpty(f)) .Distinct() .ToList(); if (!await CheckMounts(library.Name, seriesFolderPaths)) { - _logger.LogCritical( + logger.LogCritical( "Some of the root folders for library are not accessible. Please check that drives are connected and rescan. Scan will be aborted"); return ScanCancelReason.FolderMount; } if (!await CheckMounts(library.Name, libraryPaths)) { - _logger.LogCritical( + logger.LogCritical( "Some of the root folders for library are not accessible. Please check that drives are connected and rescan. Scan will be aborted"); return ScanCancelReason.FolderMount; } @@ -410,17 +368,17 @@ public class ScannerService : IScannerService if (!bypassFolderChecks) { - var allFolders = seriesFolderPaths.SelectMany(path => _directoryService.GetDirectories(path)).ToList(); + var allFolders = seriesFolderPaths.SelectMany(path => directoryService.GetDirectories(path)).ToList(); allFolders.AddRange(seriesFolderPaths); try { - if (allFolders.TrueForAll(folder => _directoryService.GetLastWriteTime(folder) <= series.LastFolderScanned)) + if (allFolders.TrueForAll(folder => directoryService.GetLastWriteTime(folder) <= series.LastFolderScanned)) { - _logger.LogInformation( + logger.LogInformation( "[ScannerService] {SeriesName} scan has no work to do. All folders have not been changed since last scan", series.Name); - await _eventHub.SendMessageAsync(MessageFactory.Info, + await eventHub.SendMessageAsync(MessageFactory.Info, MessageFactory.InfoEvent($"{series.Name} scan has no work to do", $"All folders have not been changed since last scan ({series.LastFolderScanned.ToString(CultureInfo.CurrentCulture)}). Scan will be aborted.")); return ScanCancelReason.NoChange; @@ -429,9 +387,9 @@ public class ScannerService : IScannerService catch (IOException ex) { // If there is an exception it means that the folder doesn't exist. So we should delete the series - _logger.LogError(ex, "[ScannerService] Scan series for {SeriesName} found the folder path no longer exists", + logger.LogError(ex, "[ScannerService] Scan series for {SeriesName} found the folder path no longer exists", series.Name); - await _eventHub.SendMessageAsync(MessageFactory.Info, + await eventHub.SendMessageAsync(MessageFactory.Info, MessageFactory.ErrorEvent($"{series.Name} scan has no work to do", "The folder the series was in is missing. Delete series manually or perform a library scan.")); return ScanCancelReason.NoCancel; @@ -453,10 +411,10 @@ public class ScannerService : IScannerService private async Task CommitAndSend(int seriesCount, Stopwatch sw, long scanElapsedTime, Series series) { - if (_unitOfWork.HasChanges()) + if (unitOfWork.HasChanges()) { - await _unitOfWork.CommitAsync(); - _logger.LogInformation( + await unitOfWork.CommitAsync(); + logger.LogInformation( "Processed files and {SeriesCount} series in {ElapsedScanTime} milliseconds for {SeriesName}", seriesCount, sw.ElapsedMilliseconds + scanElapsedTime, series.Name); } @@ -471,28 +429,28 @@ public class ScannerService : IScannerService private async Task CheckMounts(string libraryName, IList folders) { // Check if any of the folder roots are not available (ie disconnected from network, etc) and fail if any of them are - if (folders.Any(f => !_directoryService.IsDriveMounted(f))) + if (folders.Any(f => !directoryService.IsDriveMounted(f))) { - _logger.LogCritical("[ScannerService] Some of the root folders for library ({LibraryName} are not accessible. Please check that drives are connected and rescan. Scan will be aborted", libraryName); + logger.LogCritical("[ScannerService] Some of the root folders for library ({LibraryName} are not accessible. Please check that drives are connected and rescan. Scan will be aborted", libraryName); - await _eventHub.SendMessageAsync(MessageFactory.Error, + await eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent("Some of the root folders for library are not accessible. Please check that drives are connected and rescan. Scan will be aborted", - string.Join(", ", folders.Where(f => !_directoryService.IsDriveMounted(f))))); + string.Join(", ", folders.Where(f => !directoryService.IsDriveMounted(f))))); return false; } // For Docker instances check if any of the folder roots are not available (ie disconnected volumes, etc) and fail if any of them are - if (folders.Any(f => _directoryService.IsDirectoryEmpty(f))) + if (folders.Any(f => directoryService.IsDirectoryEmpty(f))) { // That way logging and UI informing is all in one place with full context - _logger.LogError("[ScannerService] Some of the root folders for the library are empty. " + + logger.LogError("[ScannerService] Some of the root folders for the library are empty. " + "Either your mount has been disconnected or you are trying to delete all series in the library. " + "Scan has been aborted. " + "Check that your mount is connected or change the library's root folder and rescan"); - await _eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent( $"Some of the root folders for the library, {libraryName}, are empty.", + await eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent( $"Some of the root folders for the library, {libraryName}, are empty.", "Either your mount has been disconnected or you are trying to delete all series in the library. " + "Scan has been aborted. " + "Check that your mount is connected or change the library's root folder and rescan")); @@ -508,21 +466,21 @@ public class ScannerService : IScannerService [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] public async Task ScanLibraries(bool forceUpdate = false) { - _logger.LogInformation("[ScannerService] Starting Scan of All Libraries, Forced: {Forced}", forceUpdate); - foreach (var lib in await _unitOfWork.LibraryRepository.GetLibrariesAsync()) + logger.LogInformation("[ScannerService] Starting Scan of All Libraries, Forced: {Forced}", forceUpdate); + foreach (var lib in await unitOfWork.LibraryRepository.GetLibrariesAsync()) { // BUG: This will trigger the first N libraries to scan over and over if there is always an interruption later in the chain if (TaskScheduler.HasScanTaskRunningForLibrary(lib.Id)) { // We don't need to send SignalR event as this is a background job that user doesn't need insight into - _logger.LogInformation("[ScannerService] Scan library invoked via nightly scan job but a task is already running for {LibraryName}. Rescheduling for 4 hours", lib.Name); + logger.LogInformation("[ScannerService] Scan library invoked via nightly scan job but a task is already running for {LibraryName}. Rescheduling for 4 hours", lib.Name); await Task.Delay(TimeSpan.FromHours(4)); } await ScanLibrary(lib.Id, forceUpdate, true); } - _logger.LogInformation("[ScannerService] Scan of All Libraries Finished"); + logger.LogInformation("[ScannerService] Scan of All Libraries Finished"); } @@ -540,7 +498,7 @@ public class ScannerService : IScannerService public async Task ScanLibrary(int libraryId, bool forceUpdate = false, bool isSingleScan = true) { var sw = Stopwatch.StartNew(); - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, + var library = await unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.Folders | LibraryIncludes.FileTypes | LibraryIncludes.ExcludePatterns); var libraryFolderPaths = library!.Folders.Select(fp => fp.Path).ToList(); @@ -548,92 +506,92 @@ public class ScannerService : IScannerService // Validations are done, now we can start actual scan - _logger.LogInformation("[ScannerService] Beginning file scan on {LibraryName}", library.Name); + logger.LogInformation("[ScannerService] Beginning file scan on {LibraryName}", library.Name); if (!library.EnableMetadata) { - _logger.LogInformation("[ScannerService] Warning! {LibraryName} has metadata turned off", library.Name); + logger.LogInformation("[ScannerService] Warning! {LibraryName} has metadata turned off", library.Name); } // This doesn't work for something like M:/Manga/ and a series has library folder as root - var shouldUseLibraryScan = !(await _unitOfWork.LibraryRepository.DoAnySeriesFoldersMatch(libraryFolderPaths)); + var shouldUseLibraryScan = !(await unitOfWork.LibraryRepository.DoAnySeriesFoldersMatch(libraryFolderPaths)); if (!shouldUseLibraryScan) { - _logger.LogError("[ScannerService] Library {LibraryName} consists of one or more Series folders as a library root, using series scan", library.Name); + logger.LogError("[ScannerService] Library {LibraryName} consists of one or more Series folders as a library root, using series scan", library.Name); } - _logger.LogDebug("[ScannerService] Library {LibraryName} Step 1: Scan & Parse Files", library.Name); + logger.LogDebug("[ScannerService] Library {LibraryName} Step 1: Scan & Parse Files", library.Name); var (scanElapsedTime, parsedSeries) = await ScanFiles(library, libraryFolderPaths, shouldUseLibraryScan, forceUpdate); // We need to remove any keys where there is no actual parser info - _logger.LogDebug("[ScannerService] Library {LibraryName} Step 2: Process and Update Database", library.Name); + logger.LogDebug("[ScannerService] Library {LibraryName} Step 2: Process and Update Database", library.Name); var totalFiles = await ProcessParsedSeries(forceUpdate, parsedSeries, library, scanElapsedTime); UpdateLastScanned(library); - _unitOfWork.LibraryRepository.Update(library); + unitOfWork.LibraryRepository.Update(library); - _logger.LogDebug("[ScannerService] Library {LibraryName} Step 3: Save Library", library.Name); - if (await _unitOfWork.CommitAsync()) + logger.LogDebug("[ScannerService] Library {LibraryName} Step 3: Save Library", library.Name); + if (await unitOfWork.CommitAsync()) { if (totalFiles == 0) { - _logger.LogInformation( + logger.LogInformation( "[ScannerService] Finished library scan of {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {LibraryName}. There were no changes", parsedSeries.Count, sw.ElapsedMilliseconds, library.Name); } else { - _logger.LogInformation( + logger.LogInformation( "[ScannerService] Finished library scan of {TotalFiles} files and {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {LibraryName}", totalFiles, parsedSeries.Count, sw.ElapsedMilliseconds, library.Name); } - _logger.LogDebug("[ScannerService] Library {LibraryName} Step 5: Remove Deleted Series", library.Name); + logger.LogDebug("[ScannerService] Library {LibraryName} Step 5: Remove Deleted Series", library.Name); await RemoveSeriesNotFound(parsedSeries, library); } else { - _logger.LogCritical( + logger.LogCritical( "[ScannerService] There was a critical error that resulted in a failed scan. Please check logs and rescan"); } - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Ended, string.Empty)); - await _metadataService.RemoveAbandonedMetadataKeys(); + await metadataService.RemoveAbandonedMetadataKeys(); - BackgroundJob.Enqueue(() => _directoryService.ClearDirectory(_directoryService.CacheDirectory)); + BackgroundJob.Enqueue(() => directoryService.ClearDirectory(directoryService.CacheDirectory)); } private async Task RemoveSeriesNotFound(Dictionary> parsedSeries, Library library) { try { - _logger.LogDebug("[ScannerService] Removing series that were not found during the scan"); + logger.LogDebug("[ScannerService] Removing series that were not found during the scan"); - var removedSeries = await _unitOfWork.SeriesRepository.RemoveSeriesNotInList(parsedSeries.Keys.ToList(), library.Id); - _logger.LogDebug("[ScannerService] Found {Count} series to remove: {SeriesList}", + var removedSeries = await unitOfWork.SeriesRepository.RemoveSeriesNotInList(parsedSeries.Keys.ToList(), library.Id); + logger.LogDebug("[ScannerService] Found {Count} series to remove: {SeriesList}", removedSeries.Count, string.Join(", ", removedSeries.Select(s => s.Name))); // Commit the changes - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); // Notify for each removed series foreach (var series in removedSeries) { - await _eventHub.SendMessageAsync( + await eventHub.SendMessageAsync( MessageFactory.SeriesRemoved, MessageFactory.SeriesRemovedEvent(series.Id, series.Name, series.LibraryId), false ); } - _logger.LogDebug("[ScannerService] Series removal process completed"); + logger.LogDebug("[ScannerService] Series removal process completed"); } catch (Exception ex) { - _logger.LogCritical(ex, "[ScannerService] Error during series cleanup. Please check logs and rescan"); + logger.LogCritical(ex, "[ScannerService] Error during series cleanup. Please check logs and rescan"); } } @@ -643,13 +601,13 @@ public class ScannerService : IScannerService var toProcess = new Dictionary>(); var scanSw = Stopwatch.StartNew(); - var settings = await _unitOfWork.SettingsRepository.GetMetadataSettingDto(); + var settings = await unitOfWork.SettingsRepository.GetMetadataSettingDto(); foreach (var series in parsedSeries) { if (!series.Key.HasChanged) { - _logger.LogDebug("{Series} hasn't changed", series.Key.Name); + logger.LogDebug("{Series} hasn't changed", series.Key.Name); continue; } @@ -689,11 +647,11 @@ public class ScannerService : IScannerService await CreateAllTagsAsync(processedTags); } - _logger.LogInformation("[ScannerService] Found {SeriesCount} Series that need processing in {Time} ms", toProcess.Count, scanSw.ElapsedMilliseconds + scanElapsedTime); + logger.LogInformation("[ScannerService] Found {SeriesCount} Series that need processing in {Time} ms", toProcess.Count, scanSw.ElapsedMilliseconds + scanElapsedTime); var totalFiles = await ProcessParserInfo(settings, toProcess.Values.ToList(), library, forceUpdate); - _logger.LogInformation("[ScannerService] Finished scan in {ScanAndUpdateTime} milliseconds.", scanSw.ElapsedMilliseconds + scanElapsedTime); + logger.LogInformation("[ScannerService] Finished scan in {ScanAndUpdateTime} milliseconds.", scanSw.ElapsedMilliseconds + scanElapsedTime); return totalFiles; } @@ -710,13 +668,13 @@ public class ScannerService : IScannerService { var channel = Channel.CreateUnbounded(); - var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var serverSettings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); var dbTask = Task.Run(async () => await DbMetadataTask(channel, settings, toProcess, library.Id, library.Name, forceUpdate)); var amountOfProcessors = Environment.ProcessorCount; var usingCount = Math.Max(1, amountOfProcessors / 2); - _logger.LogDebug("[ScannerService] Going to use {Cores} / {TotalCores} threads for I/O tasks this scan", + logger.LogDebug("[ScannerService] Going to use {Cores} / {TotalCores} threads for I/O tasks this scan", usingCount, amountOfProcessors); IList> tasks = []; @@ -731,7 +689,7 @@ public class ScannerService : IScannerService var totalIoTime = tasks.Select(t => t.Result).Sum(); var avgTimePerThread = totalIoTime / usingCount; - _logger.LogDebug("[ScannerService] Spend {Elapsed}ms processing covers & word count, {Average}ms per thread", + logger.LogDebug("[ScannerService] Spend {Elapsed}ms processing covers & word count, {Average}ms per thread", totalIoTime, avgTimePerThread); return (int) dbTask.Result; @@ -751,7 +709,7 @@ public class ScannerService : IScannerService await foreach (var seriesId in channel.Reader.ReadAllAsync()) { - using var scope = _scopeFactory.CreateScope(); + using var scope = scopeFactory.CreateScope(); var metadataService = scope.ServiceProvider.GetRequiredService(); var wordCountAnalyzerService = scope.ServiceProvider.GetRequiredService(); @@ -785,7 +743,7 @@ public class ScannerService : IScannerService { totalFiles += pSeries.Count; - using var scope = _scopeFactory.CreateScope(); + using var scope = scopeFactory.CreateScope(); var unitOfWork = scope.ServiceProvider.GetRequiredService(); var processSeries = scope.ServiceProvider.GetRequiredService(); @@ -813,10 +771,10 @@ public class ScannerService : IScannerService channel.Writer.Complete(); } - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.LibraryScanProgressEvent(libraryName, ProgressEventType.Ended)); - _logger.LogDebug("[ScannerService] Finished writing metadata for {Count} series in {Elapsed}ms", toProcess.Count, sw.ElapsedMilliseconds); + logger.LogDebug("[ScannerService] Finished writing metadata for {Count} series in {Elapsed}ms", toProcess.Count, sw.ElapsedMilliseconds); return totalFiles; } @@ -835,11 +793,11 @@ public class ScannerService : IScannerService private async Task>>> ScanFiles(Library library, IList dirs, bool isLibraryScan, bool forceChecks = false) { - var scanner = new ParseScannedFiles(_logger, _directoryService, _readingItemService, _eventHub); + var scanner = new ParseScannedFiles(logger, directoryService, readingItemService, eventHub); var scanWatch = Stopwatch.StartNew(); var processedSeries = await scanner.ScanLibrariesForSeries(library, dirs, - isLibraryScan, await _unitOfWork.SeriesRepository.GetFolderPathMap(library.Id), forceChecks); + isLibraryScan, await unitOfWork.SeriesRepository.GetFolderPathMap(library.Id), forceChecks); var scanElapsedTime = scanWatch.ElapsedMilliseconds; @@ -855,29 +813,29 @@ public class ScannerService : IScannerService /// private async Task CreateAllGenresAsync(ICollection genres) { - _logger.LogInformation("[ScannerService] Attempting to pre-save all Genres"); + logger.LogInformation("[ScannerService] Attempting to pre-save all Genres"); try { // Pass the non-normalized genres directly to the repository - var nonExistingGenres = await _unitOfWork.GenreRepository.GetAllGenresNotInListAsync(genres); + var nonExistingGenres = await unitOfWork.GenreRepository.GetAllGenresNotInListAsync(genres); // Create and attach new genres using the non-normalized names foreach (var genre in nonExistingGenres) { var newGenre = new GenreBuilder(genre).Build(); - _unitOfWork.GenreRepository.Attach(newGenre); + unitOfWork.GenreRepository.Attach(newGenre); } // Commit changes if (nonExistingGenres.Count > 0) { - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); } } catch (Exception ex) { - _logger.LogError(ex, "[ScannerService] There was an unknown issue when pre-saving all Genres"); + logger.LogError(ex, "[ScannerService] There was an unknown issue when pre-saving all Genres"); } } @@ -888,29 +846,29 @@ public class ScannerService : IScannerService /// private async Task CreateAllTagsAsync(ICollection tags) { - _logger.LogInformation("[ScannerService] Attempting to pre-save all Tags"); + logger.LogInformation("[ScannerService] Attempting to pre-save all Tags"); try { // Pass the non-normalized tags directly to the repository - var nonExistingTags = await _unitOfWork.TagRepository.GetAllTagsNotInListAsync(tags); + var nonExistingTags = await unitOfWork.TagRepository.GetAllTagsNotInListAsync(tags); // Create and attach new genres using the non-normalized names foreach (var tag in nonExistingTags) { var newTag = new TagBuilder(tag).Build(); - _unitOfWork.TagRepository.Attach(newTag); + unitOfWork.TagRepository.Attach(newTag); } // Commit changes if (nonExistingTags.Count > 0) { - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); } } catch (Exception ex) { - _logger.LogError(ex, "[ScannerService] There was an unknown issue when pre-saving all Tags"); + logger.LogError(ex, "[ScannerService] There was an unknown issue when pre-saving all Tags"); } } } diff --git a/API/Services/SeriesService.cs b/Kavita.Services/SeriesService.cs similarity index 81% rename from API/Services/SeriesService.cs rename to Kavita.Services/SeriesService.cs index 2324a8dca..4c01d05e2 100644 --- a/API/Services/SeriesService.cs +++ b/Kavita.Services/SeriesService.cs @@ -1,56 +1,48 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.Comparators; -using API.Data; -using API.Data.Repositories; -using API.DTOs; -using API.DTOs.Filtering; -using API.DTOs.Filtering.v2; -using API.DTOs.Person; -using API.DTOs.SeriesDetail; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; -using API.Entities.MetadataMatching; -using API.Entities.Person; -using API.Extensions; -using API.Helpers; -using API.Helpers.Builders; -using API.Helpers.Formatting; -using API.Services.Plus; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Reading; +using Kavita.API.Services.SignalR; using Kavita.Common; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Models.Builders; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Filtering; +using Kavita.Models.DTOs.Filtering.v2; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.SeriesDetail; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Metadata; +using Kavita.Models.Entities.MetadataMatching; +using Kavita.Models.Entities.Person; +using Kavita.Services.Comparators; +using Kavita.Services.Extensions; +using Kavita.Services.Helpers; +using Kavita.Services.Plus; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace API.Services; -#nullable enable +namespace Kavita.Services; -public interface ISeriesService +public class SeriesService( + IUnitOfWork unitOfWork, + IEventHub eventHub, + ITaskScheduler taskScheduler, + ILogger logger, + ILocalizationService localizationService, + IReadingListService readingListService, + IEntityNamingService namingService) + : ISeriesService { - Task GetSeriesDetail(int seriesId, int userId); - Task UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto); - Task DeleteMultipleSeries(IList seriesIds); - Task UpdateRelatedSeries(UpdateRelatedSeriesDto dto); - Task GetRelatedSeries(int userId, int seriesId); - Task GetEstimatedChapterCreationDate(int seriesId, int userId); - Task> GetCurrentlyReading(int userId, int requestingUserId, UserParams userParams); - Task> GetProfilePrivacyStatements(int userId, int requestingUserId); -} - -public class SeriesService : ISeriesService -{ - private readonly IUnitOfWork _unitOfWork; - private readonly IEventHub _eventHub; - private readonly ITaskScheduler _taskScheduler; - private readonly ILogger _logger; - private readonly ILocalizationService _localizationService; - private readonly IReadingListService _readingListService; - private readonly IEntityNamingService _namingService; - private readonly NextExpectedChapterDto _emptyExpectedChapter = new NextExpectedChapterDto { ExpectedDate = null, @@ -58,19 +50,6 @@ public class SeriesService : ISeriesService VolumeNumber = Parser.LooseLeafVolumeNumber }; - public SeriesService(IUnitOfWork unitOfWork, IEventHub eventHub, ITaskScheduler taskScheduler, - ILogger logger, ILocalizationService localizationService, IReadingListService readingListService, - IEntityNamingService namingService) - { - _unitOfWork = unitOfWork; - _eventHub = eventHub; - _taskScheduler = taskScheduler; - _logger = logger; - _localizationService = localizationService; - _readingListService = readingListService; - _namingService = namingService; - } - /// /// Returns the first chapter for a series to extract metadata from (ie Summary, etc.) /// @@ -103,13 +82,15 @@ public class SeriesService : ISeriesService /// Updates the Series Metadata. /// /// + /// /// - public async Task UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto) + public async Task UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto, + CancellationToken ct = default) { try { var seriesId = updateSeriesMetadataDto.SeriesMetadata.SeriesId; - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata); + var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata, ct); if (series == null) return false; series.Metadata ??= new SeriesMetadataBuilder() @@ -164,7 +145,7 @@ public class SeriesService : ISeriesService if (updateSeriesMetadataDto.SeriesMetadata?.Genres != null && updateSeriesMetadataDto.SeriesMetadata.Genres.Count != 0) { - var allGenres = (await _unitOfWork.GenreRepository.GetAllGenresByNamesAsync(updateSeriesMetadataDto.SeriesMetadata.Genres.Select(t => Parser.Normalize(t.Title)))).ToList(); + var allGenres = (await unitOfWork.GenreRepository.GetAllGenresByNamesAsync(updateSeriesMetadataDto.SeriesMetadata.Genres.Select(t => Parser.Normalize(t.Title)), ct)).ToList(); series.Metadata.Genres ??= []; GenreHelper.UpdateGenreList(updateSeriesMetadataDto.SeriesMetadata?.Genres, series, allGenres, genre => { @@ -179,8 +160,8 @@ public class SeriesService : ISeriesService if (updateSeriesMetadataDto.SeriesMetadata?.Tags is {Count: > 0}) { - var allTags = (await _unitOfWork.TagRepository - .GetAllTagsByNameAsync(updateSeriesMetadataDto.SeriesMetadata.Tags.Select(t => Parser.Normalize(t.Title)))) + var allTags = (await unitOfWork.TagRepository + .GetAllTagsByNameAsync(updateSeriesMetadataDto.SeriesMetadata.Tags.Select(t => Parser.Normalize(t.Title)), ct)) .ToList(); series.Metadata.Tags ??= []; TagHelper.UpdateTagList(updateSeriesMetadataDto.SeriesMetadata?.Tags, series, allTags, tag => @@ -197,14 +178,14 @@ public class SeriesService : ISeriesService { series.Metadata.AgeRating = updateSeriesMetadataDto.SeriesMetadata?.AgeRating ?? AgeRating.Unknown; series.Metadata.AgeRatingLocked = true; - await _readingListService.UpdateReadingListAgeRatingForSeries(series.Id, series.Metadata.AgeRating); + await readingListService.UpdateReadingListAgeRatingForSeries(series.Id, series.Metadata.AgeRating); series.Metadata.KPlusOverrides.Remove(MetadataSettingField.AgeRating); } else { if (!series.Metadata.AgeRatingLocked) { - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettingDto(); + var metadataSettings = await unitOfWork.SettingsRepository.GetMetadataSettingDto(ct); var allTags = series.Metadata.Tags.Select(t => t.Title).Concat(series.Metadata.Genres.Select(g => g.Title)); if (metadataSettings.EnableExtendedMetadataProcessing) @@ -228,79 +209,79 @@ public class SeriesService : ISeriesService // Writers if (!series.Metadata.WriterLocked || !updateSeriesMetadataDto.SeriesMetadata.WriterLocked) { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Writers, PersonRole.Writer, _unitOfWork); + await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Writers, PersonRole.Writer, unitOfWork); } // Cover Artists if (!series.Metadata.CoverArtistLocked || !updateSeriesMetadataDto.SeriesMetadata.CoverArtistLocked) { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.CoverArtists, PersonRole.CoverArtist, _unitOfWork); + await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.CoverArtists, PersonRole.CoverArtist, unitOfWork); } // Colorists if (!series.Metadata.ColoristLocked || !updateSeriesMetadataDto.SeriesMetadata.ColoristLocked) { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Colorists, PersonRole.Colorist, _unitOfWork); + await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Colorists, PersonRole.Colorist, unitOfWork); } // Editors if (!series.Metadata.EditorLocked || !updateSeriesMetadataDto.SeriesMetadata.EditorLocked) { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Editors, PersonRole.Editor, _unitOfWork); + await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Editors, PersonRole.Editor, unitOfWork); } // Inkers if (!series.Metadata.InkerLocked || !updateSeriesMetadataDto.SeriesMetadata.InkerLocked) { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Inkers, PersonRole.Inker, _unitOfWork); + await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Inkers, PersonRole.Inker, unitOfWork); } // Letterers if (!series.Metadata.LettererLocked || !updateSeriesMetadataDto.SeriesMetadata.LettererLocked) { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Letterers, PersonRole.Letterer, _unitOfWork); + await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Letterers, PersonRole.Letterer, unitOfWork); } // Pencillers if (!series.Metadata.PencillerLocked || !updateSeriesMetadataDto.SeriesMetadata.PencillerLocked) { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Pencillers, PersonRole.Penciller, _unitOfWork); + await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Pencillers, PersonRole.Penciller, unitOfWork); } // Publishers if (!series.Metadata.PublisherLocked || !updateSeriesMetadataDto.SeriesMetadata.PublisherLocked) { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Publishers, PersonRole.Publisher, _unitOfWork); + await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Publishers, PersonRole.Publisher, unitOfWork); } // Imprints if (!series.Metadata.ImprintLocked || !updateSeriesMetadataDto.SeriesMetadata.ImprintLocked) { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Imprints, PersonRole.Imprint, _unitOfWork); + await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Imprints, PersonRole.Imprint, unitOfWork); } // Teams if (!series.Metadata.TeamLocked || !updateSeriesMetadataDto.SeriesMetadata.TeamLocked) { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Teams, PersonRole.Team, _unitOfWork); + await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Teams, PersonRole.Team, unitOfWork); } // Locations if (!series.Metadata.LocationLocked || !updateSeriesMetadataDto.SeriesMetadata.LocationLocked) { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Locations, PersonRole.Location, _unitOfWork); + await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Locations, PersonRole.Location, unitOfWork); } // Translators if (!series.Metadata.TranslatorLocked || !updateSeriesMetadataDto.SeriesMetadata.TranslatorLocked) { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Translators, PersonRole.Translator, _unitOfWork); + await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Translators, PersonRole.Translator, unitOfWork); } // Characters if (!series.Metadata.CharacterLocked || !updateSeriesMetadataDto.SeriesMetadata.CharacterLocked) { - await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Characters, PersonRole.Character, _unitOfWork); + await HandlePeopleUpdateAsync(series.Metadata, updateSeriesMetadataDto.SeriesMetadata.Characters, PersonRole.Character, unitOfWork); } series.Metadata.AgeRatingLocked = updateSeriesMetadataDto.SeriesMetadata.AgeRatingLocked; @@ -324,30 +305,30 @@ public class SeriesService : ISeriesService series.Metadata.ReleaseYearLocked = updateSeriesMetadataDto.SeriesMetadata.ReleaseYearLocked; } - if (!_unitOfWork.HasChanges()) + if (!unitOfWork.HasChanges()) { return true; } - _unitOfWork.SeriesRepository.Update(series.Metadata); - await _unitOfWork.CommitAsync(); + unitOfWork.SeriesRepository.Update(series.Metadata); + await unitOfWork.CommitAsync(ct); // Trigger code to clean up tags, collections, people, etc try { - await _taskScheduler.CleanupDbEntries(); + await taskScheduler.CleanupDbEntries(); } catch (Exception ex) { - _logger.LogError(ex, "There was an issue cleaning up DB entries. This may happen if Komf is spamming updates. Nightly cleanup will work"); + logger.LogError(ex, "There was an issue cleaning up DB entries. This may happen if Komf is spamming updates. Nightly cleanup will work"); } return true; } catch (Exception ex) { - _logger.LogError(ex, "There was an exception when updating metadata"); - await _unitOfWork.RollbackAsync(); + logger.LogError(ex, "There was an exception when updating metadata"); + await unitOfWork.RollbackAsync(ct); } return false; @@ -361,7 +342,7 @@ public class SeriesService : ISeriesService /// public static async Task HandlePeopleUpdateAsync(SeriesMetadata metadata, ICollection peopleDtos, PersonRole role, IUnitOfWork unitOfWork) { - // TODO: Cleanup this code so we aren't using UnitOfWork like this + // default: Cleanup this code so we aren't using UnitOfWork like this // Normalize all names from the DTOs var normalizedNames = peopleDtos @@ -385,7 +366,7 @@ public class SeriesService : ISeriesService // Check if the person exists in the dictionary if (existingPeopleDictionary.TryGetValue(normalizedPersonName, out var p)) { - // TODO: Should I add more controls here to map back? + // default: Should I add more controls here to map back? if (personDto.AniListId > 0 && p.AniListId <= 0 && p.AniListId != personDto.AniListId) { p.AniListId = personDto.AniListId; @@ -456,12 +437,12 @@ public class SeriesService : ISeriesService } - public async Task DeleteMultipleSeries(IList seriesIds) + public async Task DeleteMultipleSeries(IList seriesIds, CancellationToken ct = default) { try { var chapterMappings = - await _unitOfWork.SeriesRepository.GetChapterIdWithSeriesIdForSeriesAsync([.. seriesIds]); + await unitOfWork.SeriesRepository.GetChapterIdWithSeriesIdForSeriesAsync([.. seriesIds], ct); var allChapterIds = new List(); foreach (var mapping in chapterMappings) @@ -470,34 +451,34 @@ public class SeriesService : ISeriesService } // NOTE: This isn't getting all the people and whatnot currently due to the lack of includes - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdsAsync(seriesIds); - _unitOfWork.SeriesRepository.Remove(series); + var series = await unitOfWork.SeriesRepository.GetSeriesByIdsAsync(seriesIds, ct: ct); + unitOfWork.SeriesRepository.Remove(series); var libraryIds = series.Select(s => s.LibraryId); - var libraries = await _unitOfWork.LibraryRepository.GetLibraryForIdsAsync(libraryIds); + var libraries = await unitOfWork.LibraryRepository.GetLibraryForIdsAsync(libraryIds, ct: ct); foreach (var library in libraries) { library.UpdateLastModified(); - _unitOfWork.LibraryRepository.Update(library); + unitOfWork.LibraryRepository.Update(library); } - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); foreach (var s in series) { - await _eventHub.SendMessageAsync(MessageFactory.SeriesRemoved, - MessageFactory.SeriesRemovedEvent(s.Id, s.Name, s.LibraryId), false); + await eventHub.SendMessageAsync(MessageFactory.SeriesRemoved, + MessageFactory.SeriesRemovedEvent(s.Id, s.Name, s.LibraryId), false, ct); } - await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters(); - await _unitOfWork.CollectionTagRepository.RemoveCollectionsWithoutSeries(); - _taskScheduler.CleanupChapters([.. allChapterIds]); + await unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters(ct); + await unitOfWork.CollectionTagRepository.RemoveCollectionsWithoutSeries(ct); + taskScheduler.CleanupChapters([.. allChapterIds]); return true; } catch (Exception ex) { - _logger.LogError(ex, "There was an issue when trying to delete multiple series"); + logger.LogError(ex, "There was an issue when trying to delete multiple series"); return false; } } @@ -507,28 +488,29 @@ public class SeriesService : ISeriesService ///
/// /// + /// /// - public async Task GetSeriesDetail(int seriesId, int userId) + public async Task GetSeriesDetail(int seriesId, int userId, CancellationToken ct = default) { - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); - if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist")); + var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId, ct); + if (series == null) throw new KavitaException(await localizationService.Translate(userId, "series-doesnt-exist")); - var libraryIds = await _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId); + var libraryIds = await unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId, ct: ct); if (!libraryIds.Contains(series.LibraryId)) throw new UnauthorizedAccessException("user-no-access-library-from-series"); - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, ct: ct); if (user!.AgeRestriction != AgeRating.NotApplicable) { - var seriesMetadata = await _unitOfWork.SeriesRepository.GetSeriesMetadata(seriesId); + var seriesMetadata = await unitOfWork.SeriesRepository.GetSeriesMetadata(seriesId, ct); if (seriesMetadata!.AgeRating > user.AgeRestriction) throw new UnauthorizedAccessException("series-restricted-age-restriction"); } - var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId); - var volumes = await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId); - var namingContext = await LocalizedNamingContext.CreateAsync(_namingService, _localizationService, userId, libraryType); + var libraryType = await unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId, ct); + var volumes = await unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId, ct: ct); + var namingContext = await LocalizedNamingContext.CreateAsync(namingService, localizationService, userId, libraryType); var bookTreatment = libraryType is LibraryType.Book or LibraryType.LightNovel; // For books, the Name of the Volume is remapped to the actual name of the book, rather than Volume number. @@ -596,7 +578,7 @@ public class SeriesService : ISeriesService StorylineChapters = storylineChapters, TotalCount = chapters.Count, UnreadCount = chapters.Count(c => c.Pages > 0 && c.PagesRead < c.Pages), - // TODO: See if we can get the ContinueFrom here + // default: See if we can get the ContinueFrom here }; } @@ -615,20 +597,22 @@ public class SeriesService : ISeriesService ///
/// /// + /// /// - public async Task GetRelatedSeries(int userId, int seriesId) + public async Task GetRelatedSeries(int userId, int seriesId, CancellationToken ct = default) { - return await _unitOfWork.SeriesRepository.GetRelatedSeries(userId, seriesId); + return await unitOfWork.SeriesRepository.GetRelatedSeries(userId, seriesId, ct); } /// /// Update the relations attached to the Series. Generates associated Sequel/Prequel pairs on target series. /// /// + /// /// - public async Task UpdateRelatedSeries(UpdateRelatedSeriesDto dto) + public async Task UpdateRelatedSeries(UpdateRelatedSeriesDto dto, CancellationToken ct = default) { - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, SeriesIncludes.Related); + var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, SeriesIncludes.Related, ct); if (series == null) return false; UpdateRelationForKind(dto.Adaptations, series.Relations.Where(r => r.RelationKind == RelationKind.Adaptation).ToList(), series, RelationKind.Adaptation); @@ -646,8 +630,8 @@ public class SeriesService : ISeriesService await UpdatePrequelSequelRelations(dto.Prequels, series, RelationKind.Prequel); await UpdatePrequelSequelRelations(dto.Sequels, series, RelationKind.Sequel); - if (!_unitOfWork.HasChanges()) return true; - return await _unitOfWork.CommitAsync(); + if (!unitOfWork.HasChanges()) return true; + return await unitOfWork.CommitAsync(ct); } /// @@ -684,7 +668,7 @@ public class SeriesService : ISeriesService await AddReciprocalRelation(series.Id, targetSeriesId, GetOppositeRelationKind(kind)); } - _unitOfWork.SeriesRepository.Update(series); + unitOfWork.SeriesRepository.Update(series); } private static RelationKind GetOppositeRelationKind(RelationKind kind) @@ -694,7 +678,7 @@ public class SeriesService : ISeriesService private async Task AddReciprocalRelation(int sourceSeriesId, int targetSeriesId, RelationKind kind) { - var targetSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(targetSeriesId, SeriesIncludes.Related); + var targetSeries = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(targetSeriesId, SeriesIncludes.Related); if (targetSeries == null) return; if (targetSeries.Relations.Any(r => r.RelationKind == kind && r.TargetSeriesId == sourceSeriesId)) @@ -708,19 +692,19 @@ public class SeriesService : ISeriesService RelationKind = kind }); - _unitOfWork.SeriesRepository.Update(targetSeries); + unitOfWork.SeriesRepository.Update(targetSeries); } private async Task RemoveReciprocalRelation(int sourceSeriesId, int targetSeriesId, RelationKind kind) { - var targetSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(targetSeriesId, SeriesIncludes.Related); + var targetSeries = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(targetSeriesId, SeriesIncludes.Related); if (targetSeries == null) return; var relationToRemove = targetSeries.Relations.FirstOrDefault(r => r.RelationKind == kind && r.TargetSeriesId == sourceSeriesId); if (relationToRemove != null) { targetSeries.Relations.Remove(relationToRemove); - _unitOfWork.SeriesRepository.Update(targetSeries); + unitOfWork.SeriesRepository.Update(targetSeries); } } @@ -755,15 +739,16 @@ public class SeriesService : ISeriesService TargetSeriesId = targetSeriesId, RelationKind = kind }); - _unitOfWork.SeriesRepository.Update(series); + unitOfWork.SeriesRepository.Update(series); } } - public async Task GetEstimatedChapterCreationDate(int seriesId, int userId) + public async Task GetEstimatedChapterCreationDate(int seriesId, int userId, + CancellationToken ct = default) { - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library); - if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist")); - if (!(await _unitOfWork.UserRepository.HasAccessToSeries(userId, seriesId))) + var series = await unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library, ct); + if (series == null) throw new KavitaException(await localizationService.Translate(userId, "series-doesnt-exist")); + if (!(await unitOfWork.UserRepository.HasAccessToSeries(userId, seriesId, ct))) { throw new UnauthorizedAccessException("user-no-access-library-from-series"); } @@ -783,7 +768,7 @@ public class SeriesService : ISeriesService // Only fetch the fields we need for calculation - avoids loading entire Chapter entities // with all their navigation properties, significantly reducing memory and query time - var chapterData = await _unitOfWork.ChapterRepository.GetChaptersForSeries(seriesId) + var chapterData = await unitOfWork.ChapterRepository.GetChaptersForSeries(seriesId) .Where(c => !c.IsSpecial) .Select(c => new { @@ -792,7 +777,7 @@ public class SeriesService : ISeriesService VolumeMinNumber = c.Volume.MinNumber }) .OrderBy(c => c.CreatedUtc) - .ToListAsync(); + .ToListAsync(cancellationToken: ct); if (chapterData.Count < minimumChaptersRequired) return _emptyExpectedChapter; @@ -903,27 +888,28 @@ public class SeriesService : ISeriesService // Manga uses "Chapter X", Comics use "Issue #X", Books use "Book X" 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.ComicVine => 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) + LibraryType.Manga => await localizationService.Translate(userId, "chapter-num", result.ChapterNumber), + LibraryType.Comic => await localizationService.Translate(userId, "issue-num", "#", result.ChapterNumber), + LibraryType.ComicVine => 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) }; } else { // Volume-only numbering - common for omnibus editions or series without chapter breaks result.VolumeNumber = (int)highestVolumeNumber + 1; - result.Title = await _localizationService.Translate(userId, "volume-num", result.VolumeNumber); + result.Title = await localizationService.Translate(userId, "volume-num", result.VolumeNumber); } return result; } - public async Task> GetCurrentlyReading(int userId, int requestingUserId, UserParams userParams) + public async Task> GetCurrentlyReading(int userId, int requestingUserId, UserParams userParams, + CancellationToken ct = default) { - var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var serverSettings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct); var filter = new FilterV2Dto { @@ -955,20 +941,21 @@ public class SeriesService : ISeriesService ], }; - filter.Statements.AddRange(await GetProfilePrivacyStatements(userId, requestingUserId)); + filter.Statements.AddRange(await GetProfilePrivacyStatements(userId, requestingUserId, ct)); - return await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, userParams, filter); + return await unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, userParams, filter, ct: ct); } - public async Task> GetProfilePrivacyStatements(int userId, int requestingUserId) + public async Task> GetProfilePrivacyStatements(int userId, int requestingUserId, + CancellationToken ct = default) { if (userId == requestingUserId) return []; - var socialPreferences = await _unitOfWork.UserRepository.GetSocialPreferencesForUser(userId); - var requestingUser = (await _unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId))!; + var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId, ct); + var requestingUser = (await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId, ct: ct))!; - var librariesUser = await _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId); - var librariesRequestingUser = await _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(requestingUserId); + var librariesUser = await unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId, ct: ct); + var librariesRequestingUser = await unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(requestingUserId, ct: ct); var libIds = librariesRequestingUser.Intersect(librariesUser); if (socialPreferences.SocialLibraries.Count > 0) diff --git a/API/Services/SettingsService.cs b/Kavita.Services/SettingsService.cs similarity index 78% rename from API/Services/SettingsService.cs rename to Kavita.Services/SettingsService.cs index 9f961ee68..fd41eca0b 100644 --- a/API/Services/SettingsService.cs +++ b/Kavita.Services/SettingsService.cs @@ -4,79 +4,50 @@ using System.Linq; using System.Net; using System.Security.Claims; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.DTOs; -using API.DTOs.KavitaPlus.Metadata; -using API.DTOs.Settings; -using API.Entities; -using API.Entities.Enums; -using API.Entities.MetadataMatching; -using API.Extensions; -using API.Logging; -using API.Services.Tasks.Scanner; using Flurl.Http; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Scanner; using Kavita.Common; using Kavita.Common.EnvironmentInfo; +using Kavita.Common.Extensions; using Kavita.Common.Helpers; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.KavitaPlus.Metadata; +using Kavita.Models.DTOs.Settings; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Protocols.OpenIdConnect; -namespace API.Services; +namespace Kavita.Services; -public interface ISettingsService + +public class SettingsService( + IUnitOfWork unitOfWork, + IDirectoryService directoryService, + ILibraryWatcher libraryWatcher, + ITaskScheduler taskScheduler, + ILogger logger, + IOidcService oidcService, + ILoggingService loggingService) + : ISettingsService { - Task UpdateMetadataSettings(MetadataSettingsDto dto); - /// - /// Update , , , - /// with data from the given dto. - /// - /// - /// - /// - Task ImportFieldMappings(FieldMappingsDto dto, ImportSettingsDto settings); - Task UpdateSettings(ServerSettingDto updateSettingsDto); - /// - /// Check if the server can reach the authority at the given uri - /// - /// - /// - Task IsValidAuthority(string authority); -} - - -public class SettingsService : ISettingsService -{ - private readonly IUnitOfWork _unitOfWork; - private readonly IDirectoryService _directoryService; - private readonly ILibraryWatcher _libraryWatcher; - private readonly ITaskScheduler _taskScheduler; - private readonly ILogger _logger; - private readonly IOidcService _oidcService; private readonly bool _isDevelopment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == Environments.Development; - public SettingsService(IUnitOfWork unitOfWork, IDirectoryService directoryService, - ILibraryWatcher libraryWatcher, ITaskScheduler taskScheduler, - ILogger logger, IOidcService oidcService) - { - _unitOfWork = unitOfWork; - _directoryService = directoryService; - _libraryWatcher = libraryWatcher; - _taskScheduler = taskScheduler; - _logger = logger; - _oidcService = oidcService; - } - /// /// Update the metadata settings for Kavita+ Metadata feature /// /// + /// /// - public async Task UpdateMetadataSettings(MetadataSettingsDto dto) + public async Task UpdateMetadataSettings(MetadataSettingsDto dto, CancellationToken ct = default) { - var existingMetadataSetting = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var existingMetadataSetting = await unitOfWork.SettingsRepository.GetMetadataSettings(ct); existingMetadataSetting.Enabled = dto.Enabled; existingMetadataSetting.EnableExtendedMetadataProcessing = dto.EnableExtendedMetadataProcessing; existingMetadataSetting.EnableSummary = dto.EnableSummary; @@ -107,7 +78,7 @@ public class SettingsService : ISettingsService // Clear existing mappings existingMetadataSetting.FieldMappings ??= []; - _unitOfWork.SettingsRepository.RemoveRange(existingMetadataSetting.FieldMappings); + unitOfWork.SettingsRepository.RemoveRange(existingMetadataSetting.FieldMappings); existingMetadataSetting.FieldMappings.Clear(); if (dto.FieldMappings != null) @@ -127,13 +98,14 @@ public class SettingsService : ISettingsService } // Save changes - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); // Return updated settings - return await _unitOfWork.SettingsRepository.GetMetadataSettingDto(); + return await unitOfWork.SettingsRepository.GetMetadataSettingDto(ct); } - public async Task ImportFieldMappings(FieldMappingsDto dto, ImportSettingsDto settings) + public async Task ImportFieldMappings(FieldMappingsDto dto, + ImportSettingsDto settings, CancellationToken ct = default) { if (dto.AgeRatingMappings.Keys.Distinct().Count() != dto.AgeRatingMappings.Count) { @@ -161,7 +133,7 @@ public class SettingsService : ISettingsService /// private async Task ReplaceFieldMappings(FieldMappingsDto dto, ImportSettingsDto settings) { - var existingMetadataSetting = await _unitOfWork.SettingsRepository.GetMetadataSettingDto(); + var existingMetadataSetting = await unitOfWork.SettingsRepository.GetMetadataSettingDto(); if (settings.Whitelist) { @@ -199,7 +171,7 @@ public class SettingsService : ISettingsService /// private async Task MergeFieldMappings(FieldMappingsDto dto, ImportSettingsDto settings) { - var existingMetadataSetting = await _unitOfWork.SettingsRepository.GetMetadataSettingDto(); + var existingMetadataSetting = await unitOfWork.SettingsRepository.GetMetadataSettingDto(); if (settings.Whitelist) { @@ -281,26 +253,27 @@ public class SettingsService : ISettingsService /// Update Server Settings /// /// + /// /// /// - public async Task UpdateSettings(ServerSettingDto updateSettingsDto) + public async Task UpdateSettings(ServerSettingDto updateSettingsDto, CancellationToken ct = default) { // We do not allow CacheDirectory changes, so we will ignore. - var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync(); + var currentSettings = await unitOfWork.SettingsRepository.GetSettingsAsync(ct); var updateBookmarks = false; - var originalBookmarkDirectory = _directoryService.BookmarkDirectory; + var originalBookmarkDirectory = directoryService.BookmarkDirectory; var bookmarkDirectory = updateSettingsDto.BookmarksDirectory; if (!updateSettingsDto.BookmarksDirectory.EndsWith("bookmarks") && !updateSettingsDto.BookmarksDirectory.EndsWith("bookmarks/")) { bookmarkDirectory = - _directoryService.FileSystem.Path.Join(updateSettingsDto.BookmarksDirectory, "bookmarks"); + directoryService.FileSystem.Path.Join(updateSettingsDto.BookmarksDirectory, "bookmarks"); } if (string.IsNullOrEmpty(updateSettingsDto.BookmarksDirectory)) { - bookmarkDirectory = _directoryService.BookmarkDirectory; + bookmarkDirectory = directoryService.BookmarkDirectory; } var updateTask = false; @@ -311,14 +284,14 @@ public class SettingsService : ISettingsService updateSettingsDto.OnDeckProgressDays + string.Empty != setting.Value) { setting.Value = updateSettingsDto.OnDeckProgressDays + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.OnDeckUpdateDays && updateSettingsDto.OnDeckUpdateDays + string.Empty != setting.Value) { setting.Value = updateSettingsDto.OnDeckUpdateDays + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.Port && updateSettingsDto.Port + string.Empty != setting.Value) @@ -327,7 +300,7 @@ public class SettingsService : ISettingsService setting.Value = updateSettingsDto.Port + string.Empty; // Port is managed in appSetting.json Configuration.Port = updateSettingsDto.Port; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.CacheSize && @@ -336,7 +309,7 @@ public class SettingsService : ISettingsService setting.Value = updateSettingsDto.CacheSize + string.Empty; // CacheSize is managed in appSetting.json Configuration.CacheSize = updateSettingsDto.CacheSize; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } updateTask = updateTask || UpdateSchedulingSettings(setting, updateSettingsDto); @@ -361,7 +334,7 @@ public class SettingsService : ISettingsService setting.Value = updateSettingsDto.IpAddresses; // IpAddresses is managed in appSetting.json Configuration.IpAddresses = updateSettingsDto.IpAddresses; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.BaseUrl && updateSettingsDto.BaseUrl + string.Empty != setting.Value) @@ -374,56 +347,56 @@ public class SettingsService : ISettingsService : path; setting.Value = path; Configuration.BaseUrl = updateSettingsDto.BaseUrl; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } 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); + loggingService.SwitchLogLevel(updateSettingsDto.LoggingLevel); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.EnableOpds && updateSettingsDto.EnableOpds + string.Empty != setting.Value) { setting.Value = updateSettingsDto.EnableOpds + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.EncodeMediaAs && ((int)updateSettingsDto.EncodeMediaAs).ToString() != setting.Value) { setting.Value = ((int)updateSettingsDto.EncodeMediaAs).ToString(); - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.CoverImageSize && ((int)updateSettingsDto.CoverImageSize).ToString() != setting.Value) { setting.Value = ((int)updateSettingsDto.CoverImageSize).ToString(); - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.PdfRenderResolution && ((int)updateSettingsDto.PdfRenderResolution).ToString() != setting.Value) { setting.Value = ((int)updateSettingsDto.PdfRenderResolution).ToString(); - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.HostName && updateSettingsDto.HostName + string.Empty != setting.Value) { setting.Value = (updateSettingsDto.HostName + string.Empty).Trim(); setting.Value = UrlHelper.RemoveEndingSlash(setting.Value); - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.BookmarkDirectory && bookmarkDirectory != setting.Value) { // Validate new directory can be used - if (!await _directoryService.CheckWriteAccess(bookmarkDirectory)) + if (!await directoryService.CheckWriteAccess(bookmarkDirectory)) { throw new KavitaException("bookmark-dir-permissions"); } @@ -431,8 +404,8 @@ public class SettingsService : ISettingsService originalBookmarkDirectory = setting.Value; // Normalize the path deliminators. Just to look nice in DB, no functionality - setting.Value = _directoryService.FileSystem.Path.GetFullPath(bookmarkDirectory); - _unitOfWork.SettingsRepository.Update(setting); + setting.Value = directoryService.FileSystem.Path.GetFullPath(bookmarkDirectory); + unitOfWork.SettingsRepository.Update(setting); updateBookmarks = true; } @@ -441,7 +414,7 @@ public class SettingsService : ISettingsService updateSettingsDto.AllowStatCollection + string.Empty != setting.Value) { setting.Value = updateSettingsDto.AllowStatCollection + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.TotalBackups && @@ -453,7 +426,7 @@ public class SettingsService : ISettingsService } setting.Value = updateSettingsDto.TotalBackups + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.TotalLogs && @@ -465,30 +438,30 @@ public class SettingsService : ISettingsService } setting.Value = updateSettingsDto.TotalLogs + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } if (setting.Key == ServerSettingKey.EnableFolderWatching && updateSettingsDto.EnableFolderWatching + string.Empty != setting.Value) { setting.Value = updateSettingsDto.EnableFolderWatching + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); } } - if (!_unitOfWork.HasChanges()) return updateSettingsDto; + if (!unitOfWork.HasChanges()) return updateSettingsDto; try { - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); if (!updateSettingsDto.AllowStatCollection) { - _taskScheduler.CancelStatsTasks(); + taskScheduler.CancelStatsTasks(); } else { - await _taskScheduler.ScheduleStatsTasks(); + await taskScheduler.ScheduleStatsTasks(); } if (updateBookmarks) @@ -498,7 +471,7 @@ public class SettingsService : ISettingsService if (updateTask) { - BackgroundJob.Enqueue(() => _taskScheduler.ScheduleTasks()); + BackgroundJob.Enqueue(() => taskScheduler.ScheduleTasks()); } if (updatedOidcSettings) @@ -514,27 +487,27 @@ public class SettingsService : ISettingsService if (updateSettingsDto.EnableFolderWatching) { - BackgroundJob.Enqueue(() => _libraryWatcher.StartWatching()); + BackgroundJob.Enqueue(() => libraryWatcher.StartWatching()); } else { - BackgroundJob.Enqueue(() => _libraryWatcher.StopWatching()); + BackgroundJob.Enqueue(() => libraryWatcher.StopWatching()); } } catch (Exception ex) { - _logger.LogError(ex, "There was an exception when updating server settings"); - await _unitOfWork.RollbackAsync(); + logger.LogError(ex, "There was an exception when updating server settings"); + await unitOfWork.RollbackAsync(ct); throw new KavitaException("generic-error"); } - _logger.LogInformation("Server Settings updated"); + logger.LogInformation("Server Settings updated"); return updateSettingsDto; } - public async Task IsValidAuthority(string authority) + public async Task IsValidAuthority(string authority, CancellationToken ct = default) { if (string.IsNullOrEmpty(authority)) { @@ -551,22 +524,22 @@ public class SettingsService : ISettingsService var hasTrailingSlash = authority.EndsWith('/'); var url = authority + (hasTrailingSlash ? string.Empty : "/") + ".well-known/openid-configuration"; - var json = await url.GetStringAsync(); + var json = await url.GetStringAsync(cancellationToken: ct); var config = OpenIdConnectConfiguration.Create(json); return config.Issuer == authority; } catch (Exception e) { - _logger.LogDebug(e, "OpenIdConfiguration failed: {Reason}", e.Message); + logger.LogDebug(e, "OpenIdConfiguration failed: {Reason}", e.Message); return false; } } private void UpdateBookmarkDirectory(string originalBookmarkDirectory, string bookmarkDirectory) { - _directoryService.ExistOrCreate(bookmarkDirectory); - _directoryService.CopyDirectoryToDirectory(originalBookmarkDirectory, bookmarkDirectory); - _directoryService.ClearAndDeleteDirectory(originalBookmarkDirectory); + directoryService.ExistOrCreate(bookmarkDirectory); + directoryService.CopyDirectoryToDirectory(originalBookmarkDirectory, bookmarkDirectory); + directoryService.ClearAndDeleteDirectory(originalBookmarkDirectory); } private bool UpdateSchedulingSettings(ServerSetting setting, ServerSettingDto updateSettingsDto) @@ -574,7 +547,7 @@ public class SettingsService : ISettingsService if (setting.Key == ServerSettingKey.TaskBackup && updateSettingsDto.TaskBackup != setting.Value) { setting.Value = updateSettingsDto.TaskBackup; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); return true; } @@ -582,14 +555,14 @@ public class SettingsService : ISettingsService if (setting.Key == ServerSettingKey.TaskScan && updateSettingsDto.TaskScan != setting.Value) { setting.Value = updateSettingsDto.TaskScan; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); return true; } if (setting.Key == ServerSettingKey.TaskCleanup && updateSettingsDto.TaskCleanup != setting.Value) { setting.Value = updateSettingsDto.TaskCleanup; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); return true; } return false; @@ -631,12 +604,12 @@ public class SettingsService : ISettingsService throw new KavitaException("oidc-invalid-authority"); } - _logger.LogWarning("OIDC Authority is changing, clearing all external ids"); - await _oidcService.ClearOidcIds(); + logger.LogWarning("OIDC Authority is changing, clearing all external ids"); + await oidcService.ClearOidcIds(); } setting.Value = newValue; - _unitOfWork.SettingsRepository.Update(setting); + unitOfWork.SettingsRepository.Update(setting); return true; } @@ -647,63 +620,63 @@ public class SettingsService : ISettingsService updateSettingsDto.SmtpConfig.Host + string.Empty != setting.Value) { setting.Value = updateSettingsDto.SmtpConfig.Host + string.Empty; - _unitOfWork.SettingsRepository.Update(setting); + 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); + 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); + 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); + 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); + 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); + 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); + 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); + 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); + unitOfWork.SettingsRepository.Update(setting); } } } diff --git a/Kavita.Services/SignalR/EventHub.cs b/Kavita.Services/SignalR/EventHub.cs new file mode 100644 index 000000000..cbe0e2bae --- /dev/null +++ b/Kavita.Services/SignalR/EventHub.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Kavita.API.Database; +using Kavita.API.Services.SignalR; +using Kavita.Models.DTOs.SignalR; +using Microsoft.AspNetCore.SignalR; + +namespace Kavita.Services.SignalR; + +public class EventHub(IHubContext messageHub, IPresenceTracker presenceTracker, IUnitOfWork unitOfWork) + : IEventHub +{ + // TODO: When sending a message, queue the message up and on re-connect, reply the queued messages. Queue messages expire on a rolling basis (rolling array) + + public async Task SendMessageAsync(string method, SignalRMessage message, bool onlyAdmins = true, CancellationToken ct = default) + { + var users = messageHub.Clients.All; + if (onlyAdmins) + { + var admins = await presenceTracker.GetOnlineAdminIds(); + users = messageHub.Clients.Users(admins.Select(i => i.ToString()).ToArray()); + } + else + { + users = await FilterClientsIfNeeded(users, message, ct); + } + + await users.SendAsync(method, message, cancellationToken: ct); + } + + private async Task FilterClientsIfNeeded(IClientProxy proxy, SignalRMessage message, CancellationToken ct) + { + var libraryId = GetBodyProperty(message.Body, "LibraryId"); + var seriesId = GetBodyProperty(message.Body, "SeriesId"); + + if (!libraryId.HasValue && !seriesId.HasValue) return proxy; + + var admins = await presenceTracker.GetOnlineAdminIds(); + var nonAdmins = await presenceTracker.GetOnlineUserIds(); + + List usersWithAccess = []; + + if (seriesId.HasValue) + { + foreach (var user in nonAdmins) + { + if (await unitOfWork.UserRepository.HasAccessToSeries(user, seriesId.Value, ct)) + usersWithAccess.Add(user); + } + } + else if (libraryId.HasValue) + { + foreach (var user in nonAdmins) + { + if (await unitOfWork.UserRepository.HasAccessToLibrary(user, libraryId.Value, ct)) + usersWithAccess.Add(user); + } + } + + usersWithAccess.AddRange(admins); + + return messageHub.Clients.Users(usersWithAccess.Select(i => i.ToString()).ToArray()); + } + + private static T? GetBodyProperty(object? body, string propertyName) + { + if (body is null) return default; + + var value = body.GetType() + .GetProperty(propertyName) + ?.GetValue(body); + + return value is T typed ? typed : default; + } + + + /// + /// Sends a message directly to a user if they are connected + /// + /// + /// + /// + /// + /// + public async Task SendMessageToAsync(string method, SignalRMessage message, int userId, CancellationToken ct = default) + { + await messageHub.Clients.Users([userId + string.Empty]).SendAsync(method, message, cancellationToken: ct); + } + +} diff --git a/API/SignalR/LogHub.cs b/Kavita.Services/SignalR/LogHub.cs similarity index 84% rename from API/SignalR/LogHub.cs rename to Kavita.Services/SignalR/LogHub.cs index 3a79eed0b..44e592800 100644 --- a/API/SignalR/LogHub.cs +++ b/Kavita.Services/SignalR/LogHub.cs @@ -1,17 +1,16 @@ using System; using System.Threading.Tasks; -using API.Extensions; -using API.Middleware; -using API.SignalR.Presence; +using Kavita.API.Attributes; +using Kavita.API.Services.SignalR; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs.SignalR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; +using Serilog.Sinks.AspNetCore.SignalR.Interfaces; -namespace API.SignalR; -#nullable enable +namespace Kavita.Services.SignalR; -public interface ILogHub : Serilog.Sinks.AspNetCore.SignalR.Interfaces.IHub -{ -} +public interface ILogHub : IHub; [Authorize] [SkipDeviceTracking] diff --git a/API/SignalR/MessageHub.cs b/Kavita.Services/SignalR/MessageHub.cs similarity index 87% rename from API/SignalR/MessageHub.cs rename to Kavita.Services/SignalR/MessageHub.cs index 453fff1e4..eae8ab48a 100644 --- a/API/SignalR/MessageHub.cs +++ b/Kavita.Services/SignalR/MessageHub.cs @@ -1,13 +1,13 @@ using System; using System.Threading.Tasks; -using API.Extensions; -using API.Middleware; -using API.SignalR.Presence; +using Kavita.API.Attributes; +using Kavita.API.Services.SignalR; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs.SignalR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; -namespace API.SignalR; -#nullable enable +namespace Kavita.Services.SignalR; /// /// Generic hub for sending messages to UI diff --git a/API/SignalR/Presence/PresenceTracker.cs b/Kavita.Services/SignalR/PresenceTracker.cs similarity index 86% rename from API/SignalR/Presence/PresenceTracker.cs rename to Kavita.Services/SignalR/PresenceTracker.cs index 62ab400ce..26d7b92d3 100644 --- a/API/SignalR/Presence/PresenceTracker.cs +++ b/Kavita.Services/SignalR/PresenceTracker.cs @@ -1,18 +1,10 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Data; +using Kavita.API.Database; +using Kavita.API.Services.SignalR; -namespace API.SignalR.Presence; -#nullable enable - -public interface IPresenceTracker -{ - Task UserConnected(int userId, string connectionId); - Task UserDisconnected(int userId, string connectionId); - Task GetOnlineAdminIds(); - Task> GetConnectionsForUser(int userId); -} +namespace Kavita.Services.SignalR; internal sealed record ConnectionDetail { @@ -97,6 +89,20 @@ public class PresenceTracker(IUnitOfWork unitOfWork) : IPresenceTracker return Task.FromResult(onlineUsers); } + public Task GetOnlineUserIds() + { + int[] onlineUsers; + lock (OnlineUsers) + { + onlineUsers = OnlineUsers.Where(pair => !pair.Value.IsAdmin) + .Select(k => k.Key) + .Order() + .ToArray(); + } + + return Task.FromResult(onlineUsers); + } + public Task> GetConnectionsForUser(int userId) { List? connectionIds; diff --git a/API/Services/SiteThemeService.cs b/Kavita.Services/SiteThemeService.cs similarity index 60% rename from API/Services/SiteThemeService.cs rename to Kavita.Services/SiteThemeService.cs index 6a22f724b..d9ef797e8 100644 --- a/API/Services/SiteThemeService.cs +++ b/Kavita.Services/SiteThemeService.cs @@ -3,26 +3,28 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json.Serialization; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.DTOs.Theme; -using API.Entities; -using API.Entities.Enums.Theme; -using API.Extensions; -using API.Services.Tasks.Scanner.Parser; -using API.SignalR; using Flurl.Http; using HtmlAgilityPack; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; using Kavita.Common; using Kavita.Common.EnvironmentInfo; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.DTOs.Theme; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums.Theme; +using Kavita.Services.Scanner; using MarkdownDeep; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; using Newtonsoft.Json; -namespace API.Services; -#nullable enable +namespace Kavita.Services; internal class GitHubContent { @@ -54,34 +56,19 @@ internal class ThemeMetadata public Version LastCompatible { get; set; } } - -public interface IThemeService +public class ThemeService( + IDirectoryService directoryService, + IUnitOfWork unitOfWork, + IEventHub eventHub, + ILogger logger, + IMemoryCache cache) + : IThemeService { - Task GetContent(int themeId); - Task UpdateDefault(int themeId); - /// - /// Browse theme repo for themes to download - /// - /// - Task> GetDownloadableThemes(); - - Task DownloadRepoTheme(DownloadableSiteThemeDto dto); - Task DeleteTheme(int siteThemeId); - Task CreateThemeFromFile(string tempFile, string username); - Task SyncThemes(); -} - - - -public class ThemeService : IThemeService -{ - private readonly IDirectoryService _directoryService; - private readonly IUnitOfWork _unitOfWork; - private readonly IEventHub _eventHub; - private readonly ILogger _logger; private readonly Markdown _markdown = new(); - private readonly IMemoryCache _cache; - private readonly MemoryCacheEntryOptions _cacheOptions; + + private readonly MemoryCacheEntryOptions _cacheOptions = new MemoryCacheEntryOptions() + .SetSize(1) + .SetAbsoluteExpiration(TimeSpan.FromMinutes(30)); private const string GithubBaseUrl = "https://api.github.com"; @@ -90,44 +77,31 @@ public class ThemeService : IThemeService /// private const string GithubReadme = "https://raw.githubusercontent.com/Kareadita/Themes/main/README.md"; - public ThemeService(IDirectoryService directoryService, IUnitOfWork unitOfWork, - IEventHub eventHub, ILogger logger, IMemoryCache cache) - { - _directoryService = directoryService; - _unitOfWork = unitOfWork; - _eventHub = eventHub; - _logger = logger; - _cache = cache; - - _cacheOptions = new MemoryCacheEntryOptions() - .SetSize(1) - .SetAbsoluteExpiration(TimeSpan.FromMinutes(30)); - } - /// /// Given a themeId, return the content inside that file /// /// + /// /// - public async Task GetContent(int themeId) + public async Task GetContent(int themeId, CancellationToken ct = default) { - var theme = await _unitOfWork.SiteThemeRepository.GetThemeDto(themeId) ?? throw new KavitaException("theme-doesnt-exist"); - var themeFile = _directoryService.FileSystem.Path.Join(_directoryService.SiteThemeDirectory, theme.FileName); - if (string.IsNullOrEmpty(themeFile) || !_directoryService.FileSystem.File.Exists(themeFile)) + var theme = await unitOfWork.SiteThemeRepository.GetThemeDto(themeId) ?? throw new KavitaException("theme-doesnt-exist"); + var themeFile = directoryService.FileSystem.Path.Join(directoryService.SiteThemeDirectory, theme.FileName); + if (string.IsNullOrEmpty(themeFile) || !directoryService.FileSystem.File.Exists(themeFile)) throw new KavitaException("theme-doesnt-exist"); - return await _directoryService.FileSystem.File.ReadAllTextAsync(themeFile); + return await directoryService.FileSystem.File.ReadAllTextAsync(themeFile, ct); } - public async Task> GetDownloadableThemes() + public async Task> GetDownloadableThemes(CancellationToken ct = default) { const string cacheKey = "browse"; // Avoid a duplicate Dark issue some users faced during migration - var existingThemes = (await _unitOfWork.SiteThemeRepository.GetThemeDtos()) + var existingThemes = (await unitOfWork.SiteThemeRepository.GetThemeDtos()) .GroupBy(k => k.Name) .ToDictionary(g => g.Key, g => g.First()); - if (_cache.TryGetValue(cacheKey, out List? themes) && themes != null) + if (cache.TryGetValue(cacheKey, out List? themes) && themes != null) { foreach (var t in themes) { @@ -137,7 +111,7 @@ public class ThemeService : IThemeService } // Fetch contents of the Native Themes directory - var themesContents = await GetDirectoryContent("Native%20Themes"); + var themesContents = await GetDirectoryContent("Native%20Themes", ct); // Filter out directories var themeDirectories = themesContents.Where(c => c.Type == "dir").ToList(); @@ -151,7 +125,7 @@ public class ThemeService : IThemeService var themeName = themeDir.Name.Trim(); // Fetch contents of the theme directory - var themeContents = await GetDirectoryContent(themeDir.Path); + var themeContents = await GetDirectoryContent(themeDir.Path, ct); // Find css and preview files @@ -185,7 +159,7 @@ public class ThemeService : IThemeService themeDtos.Add(dto); } - _cache.Set(cacheKey, themeDtos, _cacheOptions); + cache.Set(cacheKey, themeDtos, _cacheOptions); return themeDtos; } @@ -198,14 +172,14 @@ public class ThemeService : IThemeService .ToList(); } - private static async Task> GetDirectoryContent(string path) + private static async Task> GetDirectoryContent(string path, CancellationToken ct = default) { var json = await $"{GithubBaseUrl}/repos/Kareadita/Themes/contents/{path}" .WithHeader(HeaderNames.Accept, "application/vnd.github+json") .WithHeader(HeaderNames.UserAgent, "Kavita") - .GetStringAsync(); + .GetStringAsync(cancellationToken: ct); - return string.IsNullOrEmpty(json) ? [] : JsonConvert.DeserializeObject>(json); + return string.IsNullOrEmpty(json) ? [] : JsonConvert.DeserializeObject>(json) ?? []; } /// @@ -215,16 +189,16 @@ public class ThemeService : IThemeService private async Task> GetReadme() { // Try and delete a Readme file if it already exists - var existingReadmeFile = _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, "README.md"); - if (_directoryService.FileSystem.File.Exists(existingReadmeFile)) + var existingReadmeFile = directoryService.FileSystem.Path.Join(directoryService.TempDirectory, "README.md"); + if (directoryService.FileSystem.File.Exists(existingReadmeFile)) { - _directoryService.DeleteFiles([existingReadmeFile]); + directoryService.DeleteFiles([existingReadmeFile]); } - var tempDownloadFile = await GithubReadme.DownloadFileAsync(_directoryService.TempDirectory); + var tempDownloadFile = await GithubReadme.DownloadFileAsync(directoryService.TempDirectory); // Read file into Markdown - var htmlContent = _markdown.Transform(await _directoryService.FileSystem.File.ReadAllTextAsync(tempDownloadFile)); + var htmlContent = _markdown.Transform(await directoryService.FileSystem.File.ReadAllTextAsync(tempDownloadFile)); var htmlDoc = new HtmlDocument(); htmlDoc.LoadHtml(htmlContent); @@ -274,12 +248,12 @@ public class ThemeService : IThemeService throw new ArgumentException("SHA cannot be null or empty for already downloaded themes."); } - _directoryService.ExistOrCreate(_directoryService.SiteThemeDirectory); - var existingTempFile = _directoryService.FileSystem.Path.Join(_directoryService.SiteThemeDirectory, - _directoryService.FileSystem.FileInfo.New(dto.CssUrl).Name); - _directoryService.DeleteFiles([existingTempFile]); + directoryService.ExistOrCreate(directoryService.SiteThemeDirectory); + var existingTempFile = directoryService.FileSystem.Path.Join(directoryService.SiteThemeDirectory, + directoryService.FileSystem.FileInfo.New(dto.CssUrl).Name); + directoryService.DeleteFiles([existingTempFile]); - var tempDownloadFile = await dto.CssUrl.DownloadFileAsync(_directoryService.TempDirectory); + var tempDownloadFile = await dto.CssUrl.DownloadFileAsync(directoryService.TempDirectory); // Validate the hash on the downloaded file // if (!_fileService.ValidateSha(tempDownloadFile, dto.Sha)) @@ -287,22 +261,22 @@ public class ThemeService : IThemeService // throw new KavitaException("Cannot download theme, hash does not match"); // } - _directoryService.CopyFileToDirectory(tempDownloadFile, _directoryService.SiteThemeDirectory); - var finalLocation = _directoryService.FileSystem.Path.Join(_directoryService.SiteThemeDirectory, dto.CssFile); + directoryService.CopyFileToDirectory(tempDownloadFile, directoryService.SiteThemeDirectory); + var finalLocation = directoryService.FileSystem.Path.Join(directoryService.SiteThemeDirectory, dto.CssFile); return finalLocation; } - public async Task DownloadRepoTheme(DownloadableSiteThemeDto dto) + public async Task DownloadRepoTheme(DownloadableSiteThemeDto dto, CancellationToken ct = default) { // Validate we don't have a collision with existing or existing doesn't already exist - var existingThemes = _directoryService.ScanFiles(_directoryService.SiteThemeDirectory, string.Empty); + var existingThemes = directoryService.ScanFiles(directoryService.SiteThemeDirectory, string.Empty); if (existingThemes.Any(f => Path.GetFileName(f) == dto.CssFile)) { // This can happen if you delete then immediately download (to refresh). We should just delete the old file and download. Users can always rollback their version with github directly - _directoryService.DeleteFiles(existingThemes.Where(f => Path.GetFileName(f) == dto.CssFile)); + directoryService.DeleteFiles(existingThemes.Where(f => Path.GetFileName(f) == dto.CssFile)); } var finalLocation = await DownloadSiteTheme(dto); @@ -312,7 +286,7 @@ public class ThemeService : IThemeService { Name = dto.Name, NormalizedName = dto.Name.ToNormalized(), - FileName = _directoryService.FileSystem.Path.GetFileName(finalLocation), + FileName = directoryService.FileSystem.Path.GetFileName(finalLocation), Provider = ThemeProvider.Custom, IsDefault = false, GitHubPath = dto.Path, @@ -322,26 +296,26 @@ public class ThemeService : IThemeService ShaHash = dto.Sha, CompatibleVersion = dto.LastCompatibleVersion, }; - _unitOfWork.SiteThemeRepository.Add(theme); + unitOfWork.SiteThemeRepository.Add(theme); - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); // Inform about the new theme - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.SiteThemeProgressEvent(_directoryService.FileSystem.Path.GetFileName(theme.FileName), theme.Name, - ProgressEventType.Ended)); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.SiteThemeProgressEvent(directoryService.FileSystem.Path.GetFileName(theme.FileName), theme.Name, + ProgressEventType.Ended), ct: ct); return theme; } - public async Task SyncThemes() + public async Task SyncThemes(CancellationToken ct = default) { - var themes = await _unitOfWork.SiteThemeRepository.GetThemes(); + var themes = await unitOfWork.SiteThemeRepository.GetThemes(); var themeMetadata = await GetReadme(); foreach (var theme in themes) { await SyncTheme(theme, themeMetadata); } - _logger.LogInformation("Sync Themes complete"); + logger.LogInformation("Sync Themes complete"); } /// @@ -354,13 +328,13 @@ public class ThemeService : IThemeService // Given a theme, first validate that it is applicable if (theme == null || theme.Provider == ThemeProvider.System || string.IsNullOrEmpty(theme.GitHubPath)) { - _logger.LogInformation("Cannot Sync {ThemeName} as it is not valid", theme?.Name); + logger.LogInformation("Cannot Sync {ThemeName} as it is not valid", theme?.Name); return; } if (new Version(theme.CompatibleVersion) > BuildInfo.Version) { - _logger.LogDebug("{ThemeName} theme supports a more up-to-date version ({Version}) of Kavita. Please update", theme.Name, theme.CompatibleVersion); + logger.LogDebug("{ThemeName} theme supports a more up-to-date version ({Version}) of Kavita. Please update", theme.Name, theme.CompatibleVersion); return; } @@ -382,50 +356,51 @@ public class ThemeService : IThemeService var hasUpdated = cssFile.Sha != theme.ShaHash; if (hasUpdated) { - _logger.LogDebug("Theme {ThemeName} is out of date, updating", theme.Name); - var tempLocation = _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, theme.FileName); + logger.LogDebug("Theme {ThemeName} is out of date, updating", theme.Name); + var tempLocation = directoryService.FileSystem.Path.Join(directoryService.TempDirectory, theme.FileName); - _directoryService.DeleteFiles([tempLocation]); + directoryService.DeleteFiles([tempLocation]); - var location = await cssFile.DownloadUrl.DownloadFileAsync(_directoryService.TempDirectory); - if (_directoryService.FileSystem.File.Exists(location)) + var location = await cssFile.DownloadUrl.DownloadFileAsync(directoryService.TempDirectory); + if (directoryService.FileSystem.File.Exists(location)) { - _directoryService.CopyFileToDirectory(location, _directoryService.SiteThemeDirectory); - _logger.LogInformation("Updated Theme on disk for {ThemeName}", theme.Name); + directoryService.CopyFileToDirectory(location, directoryService.SiteThemeDirectory); + logger.LogInformation("Updated Theme on disk for {ThemeName}", theme.Name); } } - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(); if (hasUpdated) { - await _eventHub.SendMessageAsync(MessageFactory.SiteThemeUpdated, + await eventHub.SendMessageAsync(MessageFactory.SiteThemeUpdated, MessageFactory.SiteThemeUpdatedEvent(theme.Name)); } // Send an update to refresh metadata around the themes - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.SiteThemeProgressEvent(_directoryService.FileSystem.Path.GetFileName(theme.FileName), theme.Name, + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.SiteThemeProgressEvent(directoryService.FileSystem.Path.GetFileName(theme.FileName), theme.Name, ProgressEventType.Ended)); - _logger.LogInformation("Theme Sync complete"); + logger.LogInformation("Theme Sync complete"); } /// /// Deletes a SiteTheme. The CSS file will be moved to temp/ to allow user to recover data /// /// - public async Task DeleteTheme(int siteThemeId) + /// + public async Task DeleteTheme(int siteThemeId, CancellationToken ct = default) { // Validate no one else is using this theme - var inUse = await _unitOfWork.SiteThemeRepository.IsThemeInUse(siteThemeId); + var inUse = await unitOfWork.SiteThemeRepository.IsThemeInUse(siteThemeId); if (inUse) { throw new KavitaException("errors.delete-theme-in-use"); } - var siteTheme = await _unitOfWork.SiteThemeRepository.GetTheme(siteThemeId); + var siteTheme = await unitOfWork.SiteThemeRepository.GetTheme(siteThemeId); if (siteTheme == null) return; await RemoveTheme(siteTheme); @@ -435,26 +410,28 @@ public class ThemeService : IThemeService /// This assumes a file is already in temp directory and will be used for /// /// + /// + /// /// - public async Task CreateThemeFromFile(string tempFile, string username) + public async Task CreateThemeFromFile(string tempFile, string username, CancellationToken ct = default) { - if (!_directoryService.FileSystem.File.Exists(tempFile)) + if (!directoryService.FileSystem.File.Exists(tempFile)) { - _logger.LogInformation("Unable to create theme from manual upload as file not in temp"); + logger.LogInformation("Unable to create theme from manual upload as file not in temp"); throw new KavitaException("errors.theme-manual-upload"); } - var filename = _directoryService.FileSystem.FileInfo.New(tempFile).Name; + var filename = directoryService.FileSystem.FileInfo.New(tempFile).Name; var themeName = Path.GetFileNameWithoutExtension(filename); - if (await _unitOfWork.SiteThemeRepository.GetThemeDtoByName(themeName) != null) + if (await unitOfWork.SiteThemeRepository.GetThemeDtoByName(themeName) != null) { throw new KavitaException("errors.theme-already-in-use"); } - _directoryService.CopyFileToDirectory(tempFile, _directoryService.SiteThemeDirectory); - var finalLocation = _directoryService.FileSystem.Path.Join(_directoryService.SiteThemeDirectory, filename); + directoryService.CopyFileToDirectory(tempFile, directoryService.SiteThemeDirectory); + var finalLocation = directoryService.FileSystem.Path.Join(directoryService.SiteThemeDirectory, filename); // Create a new entry and note that this is downloaded @@ -462,21 +439,21 @@ public class ThemeService : IThemeService { Name = Path.GetFileNameWithoutExtension(filename), NormalizedName = themeName.ToNormalized(), - FileName = _directoryService.FileSystem.Path.GetFileName(finalLocation), + FileName = directoryService.FileSystem.Path.GetFileName(finalLocation), Provider = ThemeProvider.Custom, IsDefault = false, Description = $"Manually uploaded via UI by {username}", PreviewUrls = string.Empty, Author = username, }; - _unitOfWork.SiteThemeRepository.Add(theme); + unitOfWork.SiteThemeRepository.Add(theme); - await _unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); // Inform about the new theme - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.SiteThemeProgressEvent(_directoryService.FileSystem.Path.GetFileName(theme.FileName), theme.Name, - ProgressEventType.Ended)); + await eventHub.SendMessageAsync(MessageFactory.NotificationProgress, + MessageFactory.SiteThemeProgressEvent(directoryService.FileSystem.Path.GetFileName(theme.FileName), theme.Name, + ProgressEventType.Ended), ct: ct); return theme; } @@ -489,63 +466,64 @@ public class ThemeService : IThemeService /// private async Task RemoveTheme(SiteTheme theme) { - _logger.LogInformation("Removing {ThemeName}. File can be found in temp/ until nightly cleanup", theme.Name); - var prefs = await _unitOfWork.UserRepository.GetAllPreferencesByThemeAsync(theme.Id); - var defaultTheme = await _unitOfWork.SiteThemeRepository.GetDefaultTheme(); + logger.LogInformation("Removing {ThemeName}. File can be found in temp/ until nightly cleanup", theme.Name); + var prefs = await unitOfWork.UserRepository.GetAllPreferencesByThemeAsync(theme.Id); + var defaultTheme = await unitOfWork.SiteThemeRepository.GetDefaultTheme(); foreach (var pref in prefs) { pref.Theme = defaultTheme; - _unitOfWork.UserRepository.Update(pref); + unitOfWork.UserRepository.Update(pref); } try { // Copy the theme file to temp for nightly removal (to give user time to reclaim if made a mistake) var existingLocation = - _directoryService.FileSystem.Path.Join(_directoryService.SiteThemeDirectory, theme.FileName); + directoryService.FileSystem.Path.Join(directoryService.SiteThemeDirectory, theme.FileName); var newLocation = - _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, theme.FileName); + directoryService.FileSystem.Path.Join(directoryService.TempDirectory, theme.FileName); - if (!_directoryService.FileSystem.File.Exists(newLocation)) + if (!directoryService.FileSystem.File.Exists(newLocation)) { - _logger.LogInformation("Copying Deleted theme file ({FileName}) to config/temp, it will be removed at midnight", theme.FileName); - _directoryService.CopyFileToDirectory(existingLocation, newLocation); + logger.LogInformation("Copying Deleted theme file ({FileName}) to config/temp, it will be removed at midnight", theme.FileName); + directoryService.CopyFileToDirectory(existingLocation, newLocation); } - _directoryService.DeleteFiles([existingLocation]); + directoryService.DeleteFiles([existingLocation]); } catch (Exception) { /* Swallow */ } - _unitOfWork.SiteThemeRepository.Remove(theme); - await _unitOfWork.CommitAsync(); + unitOfWork.SiteThemeRepository.Remove(theme); + await unitOfWork.CommitAsync(); } /// /// Updates the themeId to the default theme, all others are marked as non-default /// /// + /// /// /// If theme does not exist - public async Task UpdateDefault(int themeId) + public async Task UpdateDefault(int themeId, CancellationToken ct = default) { try { - var theme = await _unitOfWork.SiteThemeRepository.GetThemeDto(themeId); + var theme = await unitOfWork.SiteThemeRepository.GetThemeDto(themeId); if (theme == null) throw new KavitaException("theme-doesnt-exist"); - foreach (var siteTheme in await _unitOfWork.SiteThemeRepository.GetThemes()) + foreach (var siteTheme in await unitOfWork.SiteThemeRepository.GetThemes()) { siteTheme.IsDefault = (siteTheme.Id == themeId); - _unitOfWork.SiteThemeRepository.Update(siteTheme); + unitOfWork.SiteThemeRepository.Update(siteTheme); } - if (!_unitOfWork.HasChanges()) return; - await _unitOfWork.CommitAsync(); + if (!unitOfWork.HasChanges()) return; + await unitOfWork.CommitAsync(ct); } catch (Exception) { - await _unitOfWork.RollbackAsync(); + await unitOfWork.RollbackAsync(ct); throw; } } diff --git a/API/Services/StatisticService.cs b/Kavita.Services/StatisticService.cs similarity index 88% rename from API/Services/StatisticService.cs rename to Kavita.Services/StatisticService.cs index 149bfe847..912d75ffd 100644 --- a/API/Services/StatisticService.cs +++ b/Kavita.Services/StatisticService.cs @@ -1,93 +1,51 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.Data.ManualMigrations; -using API.DTOs; -using API.DTOs.Metadata; -using API.DTOs.Person; -using API.DTOs.ReadingLists; -using API.DTOs.Statistics; -using API.DTOs.Stats; -using API.DTOs.Stats.V3.ClientDevice; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Enums.UserPreferences; -using API.Extensions; -using API.Extensions.QueryExtensions; -using API.Extensions.QueryExtensions.Filtering; -using API.Helpers; -using API.Helpers.Formatting; -using API.Services.Tasks.Scanner.Parser; using AutoMapper; using AutoMapper.QueryableExtensions; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.Common.Extensions; +using Kavita.Common.Helpers; +using Kavita.Database.Extensions; +using Kavita.Database.Extensions.Filters; +using Kavita.Models.DTOs; +using Kavita.Models.DTOs.Metadata; +using Kavita.Models.DTOs.Person; +using Kavita.Models.DTOs.ReadingLists; +using Kavita.Models.DTOs.Statistics; +using Kavita.Models.DTOs.Stats; +using Kavita.Models.DTOs.Stats.V3.ClientDevice; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.UserPreferences; +using Kavita.Models.Entities.User; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using TimeZoneConverter; -namespace API.Services; -#nullable enable +namespace Kavita.Services; internal sealed record UserReadCount(int ReadingListId, int ChaptersRead); -public interface IStatisticService -{ - Task GetServerStatistics(); - Task GetUserReadStatistics(int userId, IList libraryIds); - Task>> GetYearCount(); - Task>> GetTopYears(); - Task> GetPopularDecades(); - Task>> GetPopularLibraries(); - Task>> GetPopularSeries(); - Task>> GetPopularReadingList(int take = 5); - Task>> GetPopularGenres(); - Task>> GetPopularTags(); - Task>> GetPopularPerson(PersonRole role); - Task>> GetPublicationCount(); - Task>> GetMangaFormatCount(); - Task GetFileBreakdown(); - Task> GetTopUsers(int days); - Task> GetReadingHistory(int userId); - Task>> ReadCountByDay(int userId = 0, int days = 0); - Task>> ReadCounts(StatsFilterDto filter, int userId = 0); - Task>> GetDayBreakdown(int userId = 0); - Task>> GetPagesReadCountByYear(int userId = 0); - Task>> GetWordsReadCountByYear(int userId = 0); - Task UpdateServerStatistics(); - Task> GetFilesByExtension(string fileExtension); - Task GetClientTypeBreakdown(DateTime fromDateUtc); - Task>> GetDeviceTypeCounts(DateTime fromDateUtc); - Task GetReadingActivityGraphData(StatsFilterDto filter, int userId, int year, int requestingUserId); - Task GetReadingPaceForUser(StatsFilterDto filter, int userId, int year, bool booksOnly, int requestingUserId); - Task> GetGenreBreakdownForUser(StatsFilterDto filter, int userId, int requestingUserId); - Task> GetTagBreakdownForUser(StatsFilterDto filter, int userId, int requestingUserId); - Task GetPageSpreadForUser(StatsFilterDto filter, int userId, int requestingUserId); - Task GetWordSpreadForUser(StatsFilterDto filter, int userId, int requestingUserId); - Task>> GetReadsPerMonth(StatsFilterDto filter, int userId, int requestingUserId); - Task> GetMostReadAuthors(StatsFilterDto filter, int userId, int requestingUserId); - Task GetTotalReads(int userId, int requestingUserId); - Task GetTimeReadingByHour(StatsFilterDto filter, int userId, int requestingUserId); - Task GetUserStatBar(StatsFilterDto filter, int userId, int requestingUserId); - Task> GetMostActiveUsers(StatsFilterDto filter); - Task>> GetFilesAddedOverTime(); - Task> GetReadingHistoryItems(StatsFilterDto filter, UserParams userParams, int userId, int requestingUserId); -} - /// /// Responsible for computing statistics for the server /// /// This performs raw queries and does not use a repository -public class StatisticService(ILogger logger, DataContext context, +public class StatisticService(ILogger logger, IDataContext context, IMapper mapper, IUnitOfWork unitOfWork, IEntityNamingService namingService, ILocalizationService localizationService ): IStatisticService { - public async Task GetUserReadStatistics(int userId, IList libraryIds) + public async Task GetUserReadStatistics(int userId, IList libraryIds, + CancellationToken ct = default) { if (libraryIds.Count == 0) { - libraryIds = await context.Library.GetUserLibraries(userId).ToListAsync(); + libraryIds = await context.Library.GetUserLibraries(userId).ToListAsync(cancellationToken: ct); } var activityData = await context.AppUserReadingSessionActivityData @@ -104,7 +62,7 @@ public class StatisticService(ILogger logger, DataContext cont a.LibraryId, a.ChapterId }) - .ToListAsync(); + .ToListAsync(cancellationToken: ct); var totalPagesRead = activityData.Sum(a => a.PagesRead); @@ -119,7 +77,7 @@ public class StatisticService(ILogger logger, DataContext cont .Where(s => s.AppUserId == userId) .Select(s => s.EndTimeUtc) .DefaultIfEmpty() - .MaxAsync(); + .MaxAsync(cancellationToken: ct); // Average reading time per week var earliestReadDate = activityData @@ -144,11 +102,13 @@ public class StatisticService(ILogger logger, DataContext cont AvgHoursPerWeekSpentReading = avgHoursPerWeek }; } + /// /// Returns the Release Years and their count /// + /// /// - public async Task>> GetYearCount() + public async Task>> GetYearCount(CancellationToken ct = default) { return await context.SeriesMetadata .Where(sm => sm.ReleaseYear != 0) @@ -160,10 +120,10 @@ public class StatisticService(ILogger logger, DataContext cont Count = context.SeriesMetadata.Where(sm2 => sm2.ReleaseYear == sm.Key).Distinct().Count() }) .OrderByDescending(d => d.Value) - .ToListAsync(); + .ToListAsync(ct); } - public async Task>> GetTopYears() + public async Task>> GetTopYears(CancellationToken ct = default) { return await context.SeriesMetadata .Where(sm => sm.ReleaseYear != 0) @@ -176,10 +136,10 @@ public class StatisticService(ILogger logger, DataContext cont }) .OrderByDescending(d => d.Count) .Take(5) - .ToListAsync(); + .ToListAsync(ct); } - public async Task> GetPopularDecades() + public async Task> GetPopularDecades(CancellationToken ct = default) { var decadeGroups = await context.SeriesMetadata .Where(sm => sm.ReleaseYear != 0) @@ -189,7 +149,7 @@ public class StatisticService(ILogger logger, DataContext cont Decade = g.Key, Count = g.Count() }) - .ToListAsync(); + .ToListAsync(ct); var totalCount = decadeGroups.Sum(d => d.Count); @@ -207,7 +167,7 @@ public class StatisticService(ILogger logger, DataContext cont .ToList(); } - public async Task>> GetPopularLibraries() + public async Task>> GetPopularLibraries(CancellationToken ct = default) { var counts = await context.AppUserProgresses .Where(p => p.LibraryId > 0) @@ -216,7 +176,7 @@ public class StatisticService(ILogger logger, DataContext cont var libraries = await context.Library .Where(l => counts.Select(c => c.Id).Contains(l.Id)) .ProjectTo(mapper.ConfigurationProvider) - .ToDictionaryAsync(l => l.Id); + .ToDictionaryAsync(l => l.Id, cancellationToken: ct); return counts .Where(c => libraries.ContainsKey(c.Id)) @@ -228,7 +188,7 @@ public class StatisticService(ILogger logger, DataContext cont .ToList(); } - public async Task>> GetPopularSeries() + public async Task>> GetPopularSeries(CancellationToken ct = default) { var counts = await context.AppUserProgresses .GetTopCounts(p => p.SeriesId, take: 5); @@ -239,7 +199,7 @@ public class StatisticService(ILogger logger, DataContext cont var series = await context.Series .Where(s => counts.Select(c => c.Id).Contains(s.Id)) .ProjectTo(mapper.ConfigurationProvider) - .ToDictionaryAsync(s => s.Id); + .ToDictionaryAsync(s => s.Id, cancellationToken: ct); return counts .Where(c => series.ContainsKey(c.Id)) @@ -251,7 +211,7 @@ public class StatisticService(ILogger logger, DataContext cont .ToList(); } - public async Task>> GetPopularReadingList(int take = 5) + public async Task>> GetPopularReadingList(int take = 5, CancellationToken ct = default) { var readingListChapterCounts = await context.ReadingList .Where(rl => rl.Promoted) @@ -261,7 +221,7 @@ public class StatisticService(ILogger logger, DataContext cont TotalChapters = rl.Items.Count }) .Where(x => x.TotalChapters > 0) - .ToDictionaryAsync(x => x.ReadingListId, x => x.TotalChapters); + .ToDictionaryAsync(x => x.ReadingListId, x => x.TotalChapters, cancellationToken: ct); if (readingListChapterCounts.Count == 0) return []; @@ -280,7 +240,7 @@ public class StatisticService(ILogger logger, DataContext cont .Select(g => new UserReadCount( g.Key.ReadingListId, g.Select(x => x.ChapterId).Distinct().Count())) - .ToListAsync(); + .ToListAsync(ct); if (userReadCounts.Count == 0) return []; @@ -292,7 +252,7 @@ public class StatisticService(ILogger logger, DataContext cont var readingLists = await context.ReadingList .Where(rl => readingListIds.Contains(rl.Id)) .ProjectTo(mapper.ConfigurationProvider) - .ToDictionaryAsync(rl => rl.Id); + .ToDictionaryAsync(rl => rl.Id, cancellationToken: ct); return counts .Where(c => readingLists.ContainsKey(c.ReadingListId)) @@ -334,9 +294,10 @@ public class StatisticService(ILogger logger, DataContext cont /// /// Top 5 genres where there is some reading activity /// + /// /// Since most users only tag the Series level metadata, this will only check against Series. Will count series * totalReads of series /// - public async Task>> GetPopularGenres() + public async Task>> GetPopularGenres(CancellationToken ct = default) { var counts = await context.AppUserProgresses .GetTopCounts(p => p.SeriesId); @@ -352,7 +313,7 @@ public class StatisticService(ILogger logger, DataContext cont sm.SeriesId }) .Where(x => countDict.Keys.Contains(x.SeriesId)) - .ToListAsync(); + .ToListAsync(ct); return genreStats .GroupBy(x => x.Genre) @@ -370,7 +331,7 @@ public class StatisticService(ILogger logger, DataContext cont .ToList(); } - public async Task>> GetPopularTags() + public async Task>> GetPopularTags(CancellationToken ct = default) { var counts = await context.AppUserProgresses .GetTopCounts(p => p.SeriesId); @@ -387,7 +348,7 @@ public class StatisticService(ILogger logger, DataContext cont sm.SeriesId }) .Where(x => countDict.Keys.Contains(x.SeriesId)) - .ToListAsync(); + .ToListAsync(ct); return genreStats .GroupBy(x => x.Tag) @@ -405,7 +366,7 @@ public class StatisticService(ILogger logger, DataContext cont .ToList(); } - public async Task>> GetPopularPerson(PersonRole role) + public async Task>> GetPopularPerson(PersonRole role, CancellationToken ct = default) { var counts = await context.AppUserProgresses .GetTopCounts(p => p.SeriesId); @@ -422,7 +383,7 @@ public class StatisticService(ILogger logger, DataContext cont smp.Person, smp.SeriesMetadata.SeriesId }) - .ToListAsync(); + .ToListAsync(ct); return authorStats .GroupBy(x => x.Person) @@ -446,7 +407,7 @@ public class StatisticService(ILogger logger, DataContext cont - public async Task>> GetPublicationCount() + public async Task>> GetPublicationCount(CancellationToken ct = default) { return await context.SeriesMetadata .AsSplitQuery() @@ -456,10 +417,10 @@ public class StatisticService(ILogger logger, DataContext cont Value = sm.Key, Count = context.SeriesMetadata.Where(sm2 => sm2.PublicationStatus == sm.Key).Distinct().Count() }) - .ToListAsync(); + .ToListAsync(ct); } - public async Task>> GetMangaFormatCount() + public async Task>> GetMangaFormatCount(CancellationToken ct = default) { return await context.MangaFile .AsSplitQuery() @@ -469,10 +430,10 @@ public class StatisticService(ILogger logger, DataContext cont Value = mf.Key, Count = context.MangaFile.Where(mf2 => mf2.Format == mf.Key).Distinct().Count() }) - .ToListAsync(); + .ToListAsync(ct); } - public async Task GetServerStatistics() + public async Task GetServerStatistics(CancellationToken ct = default) { var counts = await context.Chapter .Select(_ => new @@ -486,15 +447,15 @@ public class StatisticService(ILogger logger, DataContext cont Volumes = context.Volume.Count(v => Math.Abs(v.MinNumber - Parser.LooseLeafVolumeNumber) > 0.001f), TotalBytes = context.MangaFile.Sum(m => m.Bytes) }) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); if (counts == null) return new ServerStatisticsDto(); var totalReadingHours = await context.AppUserReadingSessionActivityData .Where(a => a.EndTimeUtc != null) .Select(a => new { a.StartTimeUtc, EndTimeUtc = a.EndTimeUtc!.Value }) - .ToListAsync() - .ContinueWith(t => t.Result.Sum(a => (a.EndTimeUtc - a.StartTimeUtc).TotalHours)); + .ToListAsync(ct) + .ContinueWith(t => t.Result.Sum(a => (a.EndTimeUtc - a.StartTimeUtc).TotalHours), ct); return new ServerStatisticsDto { @@ -510,7 +471,7 @@ public class StatisticService(ILogger logger, DataContext cont }; } - public async Task GetFileBreakdown() + public async Task GetFileBreakdown(CancellationToken ct = default) { return new FileExtensionBreakdownDto() { @@ -526,15 +487,15 @@ public class StatisticService(ILogger logger, DataContext cont TotalFiles = context.MangaFile.Where(mf2 => mf2.Extension == mf.Key).Distinct().Count() }) .OrderBy(d => d.TotalFiles) - .ToListAsync(), + .ToListAsync(ct), TotalFileSize = await context.MangaFile .AsNoTracking() .AsSplitQuery() - .SumAsync(f => f.Bytes) + .SumAsync(f => f.Bytes, cancellationToken: ct) }; } - public async Task> GetReadingHistory(int userId) + public async Task> GetReadingHistory(int userId, CancellationToken ct = default) { return await context.AppUserProgresses .Where(u => u.AppUserId == userId) @@ -553,10 +514,11 @@ public class StatisticService(ILogger logger, DataContext cont ChapterNumber = context.Chapter.Single(c => c.Id == u.ChapterId).MinNumber }) .OrderByDescending(d => d.ReadDate) - .ToListAsync(); + .ToListAsync(ct); } - public async Task>> ReadCountByDay(int userId = 0, int days = 0) + public async Task>> ReadCountByDay(int userId = 0, int days = 0, + CancellationToken ct = default) { var query = context.AppUserProgresses .AsSplitQuery() @@ -584,7 +546,7 @@ public class StatisticService(ILogger logger, DataContext cont x.chapter.AvgHoursToRead * (x.appUserProgresses.PagesRead / (1.0f * x.chapter.Pages))) }) .OrderBy(d => d.Value) - .ToListAsync(); + .ToListAsync(ct); if (results.Count > 0) { @@ -637,7 +599,8 @@ public class StatisticService(ILogger logger, DataContext cont return results.OrderBy(r => r.Value); } - public async Task>> ReadCounts(StatsFilterDto filter, int userId = 0) + public async Task>> ReadCounts(StatsFilterDto filter, int userId = 0, + CancellationToken ct = default) { var userTimeZone = GetTimeZoneOrUtc(filter.TimeZoneId); var startDate = filter.StartDate?.ToUniversalTime() ?? DateTime.MinValue; @@ -655,7 +618,7 @@ public class StatisticService(ILogger logger, DataContext cont EndTimeUtc = a.EndTimeUtc!.Value, a.Format }) - .ToListAsync(); + .ToListAsync(ct); var results = rawData .GroupBy(a => new @@ -723,7 +686,7 @@ public class StatisticService(ILogger logger, DataContext cont } } - public async Task>> GetDayBreakdown(int userId = 0) + public async Task>> GetDayBreakdown(int userId = 0, CancellationToken ct = default) { return await context.AppUserReadingSessionActivityData .AsNoTracking() @@ -735,13 +698,13 @@ public class StatisticService(ILogger logger, DataContext cont Value = g.Key, Count = g.Count() }) - .ToListAsync(); + .ToListAsync(ct); } /// /// Return a list of pages read per year for the given userId /// - public async Task>> GetPagesReadCountByYear(int userId = 0) + public async Task>> GetPagesReadCountByYear(int userId = 0, CancellationToken ct = default) { return await context.AppUserReadingSessionActivityData .AsNoTracking() @@ -753,13 +716,13 @@ public class StatisticService(ILogger logger, DataContext cont Value = g.Key, Count = g.Sum(a => a.PagesRead) }) - .ToListAsync(); + .ToListAsync(ct); } /// /// Return a list of words read per year for the given userId /// - public async Task>> GetWordsReadCountByYear(int userId = 0) + public async Task>> GetWordsReadCountByYear(int userId = 0, CancellationToken ct = default) { return await context.AppUserReadingSessionActivityData .AsNoTracking() @@ -772,28 +735,29 @@ public class StatisticService(ILogger logger, DataContext cont Value = g.Key, Count = g.Sum(a => a.WordsRead) }) - .ToListAsync(); + .ToListAsync(ct); } /// /// Updates the ServerStatistics table for the current year /// + /// /// This commits /// - public async Task UpdateServerStatistics() + public async Task UpdateServerStatistics(CancellationToken ct = default) { var year = DateTime.Today.Year; - var existingRecord = await context.ServerStatistics.SingleOrDefaultAsync(s => s.Year == year) ?? new ServerStatistics(); + var existingRecord = await context.ServerStatistics.SingleOrDefaultAsync(s => s.Year == year, cancellationToken: ct) ?? new ServerStatistics(); existingRecord.Year = year; - existingRecord.ChapterCount = await context.Chapter.CountAsync(); - existingRecord.VolumeCount = await context.Volume.CountAsync(); - existingRecord.FileCount = await context.MangaFile.CountAsync(); - existingRecord.SeriesCount = await context.Series.CountAsync(); - existingRecord.UserCount = await context.Users.CountAsync(); - existingRecord.GenreCount = await context.Genre.CountAsync(); - existingRecord.TagCount = await context.Tag.CountAsync(); + existingRecord.ChapterCount = await context.Chapter.CountAsync(ct); + existingRecord.VolumeCount = await context.Volume.CountAsync(ct); + existingRecord.FileCount = await context.MangaFile.CountAsync(ct); + existingRecord.SeriesCount = await context.Series.CountAsync(ct); + existingRecord.UserCount = await context.Users.CountAsync(ct); + existingRecord.GenreCount = await context.Genre.CountAsync(ct); + existingRecord.TagCount = await context.Tag.CountAsync(ct); existingRecord.PersonCount = context.Person .AsSplitQuery() .AsEnumerable() @@ -807,7 +771,7 @@ public class StatisticService(ILogger logger, DataContext cont { context.Entry(existingRecord).State = EntityState.Modified; } - await unitOfWork.CommitAsync(); + await unitOfWork.CommitAsync(ct); } public async Task TimeSpentReadingForUsersAsync(IList userIds, IList libraryIds) @@ -827,22 +791,24 @@ public class StatisticService(ILogger logger, DataContext cont p.chapter.AvgHoursToRead * (p.progress.PagesRead / (1.0f * p.chapter.Pages)))); } - public async Task> GetFilesByExtension(string fileExtension) + public async Task> GetFilesByExtension(string fileExtension, + CancellationToken ct = default) { var query = context.MangaFile .Where(f => f.Extension == fileExtension) .ProjectTo(mapper.ConfigurationProvider) .OrderBy(f => f.FilePath); - return await query.ToListAsync(); + return await query.ToListAsync(ct); } - public async Task GetClientTypeBreakdown(DateTime fromDateUtc) + public async Task GetClientTypeBreakdown(DateTime fromDateUtc, + CancellationToken ct = default) { var devices = await context.ClientDevice .Where(d => d.IsActive && d.LastSeenUtc >= fromDateUtc) .Select(d => d.CurrentClientInfo.ClientType) - .ToListAsync(); + .ToListAsync(ct); var grouped = devices .GroupBy(clientType => clientType) @@ -862,12 +828,12 @@ public class StatisticService(ILogger logger, DataContext cont } - public async Task>> GetDeviceTypeCounts(DateTime fromDateUtc) + public async Task>> GetDeviceTypeCounts(DateTime fromDateUtc, CancellationToken ct = default) { var devices = await context.ClientDevice .Where(d => d.IsActive && d.LastSeenUtc >= fromDateUtc) .Select(d => d.CurrentClientInfo.DeviceType) - .ToListAsync(); + .ToListAsync(ct); // Define the expected device types var knownDeviceTypes = new[] { "mobile", "desktop", "tablet" }; @@ -890,10 +856,13 @@ public class StatisticService(ILogger logger, DataContext cont return result; } - public async Task GetReadingActivityGraphData(StatsFilterDto filter, int userId, int year, int requestingUserId) + public async Task GetReadingActivityGraphData(StatsFilterDto filter, int userId, int year, + int requestingUserId, CancellationToken ct = default) { - var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId); - var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId); + var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId, ct); + var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId, ct: ct); + if (requestingUser == null) return new ReadingActivityGraphDto(); + var userTimeZone = GetTimeZoneOrUtc(filter.TimeZoneId); // Define year boundaries as local dates in user's timezone @@ -924,7 +893,7 @@ public class StatisticService(ILogger logger, DataContext cont activity.TotalPages, activity.EndPage, }) - .ToListAsync(); + .ToListAsync(ct); var result = new ReadingActivityGraphDto(); @@ -999,12 +968,14 @@ public class StatisticService(ILogger logger, DataContext cont } } - public async Task GetReadingPaceForUser(StatsFilterDto filter, int userId, int year, bool booksOnly, int requestingUserId) + public async Task GetReadingPaceForUser(StatsFilterDto filter, int userId, int year, bool booksOnly, + int requestingUserId, CancellationToken ct = default) { - var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId); - var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId); + var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId, ct); + var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId, ct: ct); + if (requestingUser == null) return new ReadingPaceDto(); - var firstProgress = await unitOfWork.AppUserProgressRepository.GetFirstProgressForUser(userId); + var firstProgress = await unitOfWork.AppUserProgressRepository.GetFirstProgressForUser(userId, ct); if (firstProgress == null) { return new ReadingPaceDto(); @@ -1030,7 +1001,7 @@ public class StatisticService(ILogger logger, DataContext cont }) .WhereIf(booksOnly, d => d.SeriesFormat == MangaFormat.Pdf || d.SeriesFormat == MangaFormat.Epub) .WhereIf(!booksOnly, d => d.SeriesFormat != MangaFormat.Pdf && d.SeriesFormat != MangaFormat.Epub) - .ToListAsync(); + .ToListAsync(ct); var sessionDurations = activities .Where(a => a.SessionEnd.HasValue) @@ -1069,10 +1040,12 @@ public class StatisticService(ILogger logger, DataContext cont }; } - public async Task> GetGenreBreakdownForUser(StatsFilterDto filter, int userId, int requestingUserId) + public async Task> GetGenreBreakdownForUser(StatsFilterDto filter, int userId, + int requestingUserId, CancellationToken ct = default) { - var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId); - var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId); + var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId, ct); + var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId, ct: ct); + if (requestingUser == null) return new BreakDownDto(); var readsPerGenre = await context.AppUserReadingSessionActivityData .ApplyStatsFilter(filter, userId, socialPreferences, requestingUser, onlyCompleted: false) @@ -1112,27 +1085,27 @@ public class StatisticService(ILogger logger, DataContext cont }) .OrderByDescending(x => x.Count) .Take(10) - .ToListAsync(); + .ToListAsync(ct); var totalMissingData = await context.AppUserReadingSessionActivityData .ApplyStatsFilter(filter, userId, socialPreferences, requestingUser) .Select(p => p.SeriesId) .Distinct() .Join(context.SeriesMetadata, p => p, sm => sm.SeriesId, (g, m) => m.Genres) - .CountAsync(g => !g.Any()); + .CountAsync(g => !g.Any(), cancellationToken: ct); var totalReads = await context.AppUserReadingSessionActivityData .ApplyStatsFilter(filter, userId, socialPreferences, requestingUser) .Select(p => p.SeriesId) .Distinct() - .CountAsync(); + .CountAsync(ct); var totalReadGenres = await context.AppUserReadingSessionActivityData .ApplyStatsFilter(filter, userId, socialPreferences, requestingUser) .Join(context.Chapter, p => p.ChapterId, c => c.Id, (p, c) => c.Genres) .SelectMany(g => g.Select(gg => gg.NormalizedTitle)) .Distinct() - .CountAsync(); + .CountAsync(ct); return new BreakDownDto() { @@ -1144,10 +1117,12 @@ public class StatisticService(ILogger logger, DataContext cont } - public async Task> GetTagBreakdownForUser(StatsFilterDto filter, int userId, int requestingUserId) + public async Task> GetTagBreakdownForUser(StatsFilterDto filter, int userId, + int requestingUserId, CancellationToken ct = default) { - var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId); - var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId); + var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId, ct); + var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId, ct: ct); + if (requestingUser == null) return new BreakDownDto(); var readsPerTagTask = context.AppUserReadingSessionActivityData .ApplyStatsFilter(filter, userId, socialPreferences, requestingUser, onlyCompleted: false) @@ -1187,27 +1162,27 @@ public class StatisticService(ILogger logger, DataContext cont }) .OrderByDescending(x => x.Count) .Take(10) - .ToListAsync(); + .ToListAsync(ct); var totalMissingDataTask = context.AppUserReadingSessionActivityData .ApplyStatsFilter(filter, userId, socialPreferences, requestingUser) .Select(p => p.SeriesId) .Distinct() .Join(context.SeriesMetadata, p => p, sm => sm.SeriesId, (g, m) => m.Tags) - .CountAsync(g => !g.Any()); + .CountAsync(g => !g.Any(), cancellationToken: ct); var totalReadsTask = context.AppUserReadingSessionActivityData .ApplyStatsFilter(filter, userId, socialPreferences, requestingUser) .Select(p => p.SeriesId) .Distinct() - .CountAsync(); + .CountAsync(ct); var totalReadTagsTask = context.AppUserReadingSessionActivityData .ApplyStatsFilter(filter, userId, socialPreferences, requestingUser) .Join(context.Chapter, p => p.ChapterId, c => c.Id, (p, c) => c.Tags) .SelectMany(g => g.Select(gg => gg.NormalizedTitle)) .Distinct() - .CountAsync(); + .CountAsync(ct); await Task.WhenAll(readsPerTagTask, totalMissingDataTask, totalReadsTask, totalReadTagsTask); @@ -1220,10 +1195,12 @@ public class StatisticService(ILogger logger, DataContext cont }; } - public async Task GetPageSpreadForUser(StatsFilterDto filter, int userId, int requestingUserId) + public async Task GetPageSpreadForUser(StatsFilterDto filter, int userId, int requestingUserId, + CancellationToken ct = default) { - var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId); - var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId); + var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId, ct); + var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId, ct: ct); + if (requestingUser == null) return new SpreadStatsDto(); var fullyReadChapters = await context.AppUserReadingSessionActivityData .ApplyStatsFilter(filter, userId, socialPreferences, requestingUser, isAggregate: true) @@ -1234,7 +1211,7 @@ public class StatisticService(ILogger logger, DataContext cont (progress, chapter) => new { progress, chapter } ) .Select(x => x.chapter.Pages) - .ToListAsync(); + .ToListAsync(ct); var totalCount = fullyReadChapters.Count; var highest = fullyReadChapters.MaxOrDefault(x => x, 0); @@ -1280,10 +1257,12 @@ public class StatisticService(ILogger logger, DataContext cont }; } - public async Task GetWordSpreadForUser(StatsFilterDto filter, int userId, int requestingUserId) + public async Task GetWordSpreadForUser(StatsFilterDto filter, int userId, int requestingUserId, + CancellationToken ct = default) { - var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId); - var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId); + var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId, ct); + var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId, ct: ct); + if (requestingUser == null) return new SpreadStatsDto(); var wordsInFullyReadChapters = await context.AppUserReadingSessionActivityData .ApplyStatsFilter(filter, userId, socialPreferences, requestingUser, isAggregate: true) @@ -1295,7 +1274,7 @@ public class StatisticService(ILogger logger, DataContext cont ) .Where(x => x.chapter.WordCount > 0) .Select(x => x.chapter.WordCount) - .ToListAsync(); + .ToListAsync(ct); var totalCount = wordsInFullyReadChapters.Count; var highest = wordsInFullyReadChapters.MaxOrDefault(x => x, 0); @@ -1343,18 +1322,21 @@ public class StatisticService(ILogger logger, DataContext cont } - public async Task GetTimeReadingByHour(StatsFilterDto filter, int userId, int requestingUserId) + public async Task GetTimeReadingByHour(StatsFilterDto filter, int userId, int requestingUserId, + CancellationToken ct = default) { - var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId); - var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId); + var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId, ct); + var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId, ct: ct); + if (requestingUser == null) return null; + var userTimeZone = GetTimeZoneOrUtc(filter.TimeZoneId); var sessionRecordedSince = await unitOfWork.DataContext.ManualMigrationHistory - .FirstOrDefaultAsync(mm => mm.Name == MigrateProgressToReadingSessions.Name); + .FirstOrDefaultAsync(mm => mm.Name == "MigrateProgressToReadingSessions", cancellationToken: ct); if (sessionRecordedSince == null) { - logger.LogWarning("{Migration} never happened! Cannot compute time by hour", MigrateProgressToReadingSessions.Name); + logger.LogWarning("{Migration} never happened! Cannot compute time by hour", "MigrateProgressToReadingSessions"); return null; } @@ -1369,7 +1351,7 @@ public class StatisticService(ILogger logger, DataContext cont s.StartTimeUtc, s.EndTimeUtc }) - .ToListAsync(); + .ToListAsync(ct); var hourStats = sessions .Where(s => s.EndTimeUtc.HasValue) @@ -1427,10 +1409,12 @@ public class StatisticService(ILogger logger, DataContext cont }; } - public async Task GetUserStatBar(StatsFilterDto filter, int userId, int requestingUserId) + public async Task GetUserStatBar(StatsFilterDto filter, int userId, int requestingUserId, + CancellationToken ct = default) { - var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId); - var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId); + var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId, ct); + var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId, ct: ct); + if (requestingUser == null) return new ProfileStatBarDto(); var chapterData = await context.AppUserReadingSessionActivityData .ApplyStatsFilter(filter, userId, socialPreferences, requestingUser, isAggregate: true, onlyCompleted: false) @@ -1442,13 +1426,13 @@ public class StatisticService(ILogger logger, DataContext cont d.WordsRead, Finished = d.EndPage >= d.Chapter.Pages }) - .ToListAsync(); + .ToListAsync(ct); // Early exit if no data if (chapterData.Count == 0) { // Still need reviews/ratings - run in parallel - var (reviews, ratings) = await GetReviewsAndRatings(filter, userId, socialPreferences); + var (reviews, ratings) = await GetReviewsAndRatings(filter, userId, socialPreferences, ct); return new ProfileStatBarDto { Reviews = reviews, @@ -1496,8 +1480,8 @@ public class StatisticService(ILogger logger, DataContext cont } } - var authorsTask = GetAuthorsCount(chapterIds); - var reviewsRatingsTask = GetReviewsAndRatings(filter, userId, socialPreferences); + var authorsTask = GetAuthorsCount(chapterIds, ct); + var reviewsRatingsTask = GetReviewsAndRatings(filter, userId, socialPreferences, ct); await Task.WhenAll(authorsTask, reviewsRatingsTask); @@ -1515,7 +1499,7 @@ public class StatisticService(ILogger logger, DataContext cont }; } - public async Task> GetMostActiveUsers(StatsFilterDto filter) + public async Task> GetMostActiveUsers(StatsFilterDto filter, CancellationToken ct = default) { var startDate = filter.StartDate?.ToUniversalTime() ?? DateTime.MinValue; var endDate = filter.EndDate?.ToUniversalTime() ?? DateTime.UtcNow; @@ -1533,7 +1517,7 @@ public class StatisticService(ILogger logger, DataContext cont a.StartTimeUtc, EndTimeUtc = a.EndTimeUtc!.Value }) - .ToListAsync(); + .ToListAsync(ct); if (activityData.Count == 0) return []; @@ -1584,7 +1568,7 @@ public class StatisticService(ILogger logger, DataContext cont var users = await context.AppUser .Where(u => userIds.Contains(u.Id)) .Select(u => new { u.Id, u.UserName, u.CoverImage }) - .ToDictionaryAsync(u => u.Id); + .ToDictionaryAsync(u => u.Id, cancellationToken: ct); // Fetch TotalReads for each user's series var allSeriesIds = userStats @@ -1601,7 +1585,7 @@ public class StatisticService(ILogger logger, DataContext cont g.Key.SeriesId, MinTotalReads = g.Min(p => p.TotalReads) }) - .ToListAsync(); + .ToListAsync(ct); var progressLookup = progressData.ToLookup(p => p.AppUserId); @@ -1609,7 +1593,7 @@ public class StatisticService(ILogger logger, DataContext cont var seriesLookup = await context.Series .Where(s => allSeriesIds.Contains(s.Id)) .ProjectTo(mapper.ConfigurationProvider) - .ToDictionaryAsync(s => s.Id); + .ToDictionaryAsync(s => s.Id, cancellationToken: ct); var result = new List(); foreach (var stat in userStats) @@ -1642,11 +1626,11 @@ public class StatisticService(ILogger logger, DataContext cont return result; } - public async Task>> GetFilesAddedOverTime() + public async Task>> GetFilesAddedOverTime(CancellationToken ct = default) { var results = await context.MangaFile .AsNoTracking() - .GroupBy(f => new { Date = f.CreatedUtc.Date, f.Format }) + .GroupBy(f => new { f.CreatedUtc.Date, f.Format }) .Select(g => new StatCountWithFormat { Value = g.Key.Date, @@ -1654,15 +1638,18 @@ public class StatisticService(ILogger logger, DataContext cont Format = g.Key.Format }) .OrderBy(d => d.Value) - .ToListAsync(); + .ToListAsync(ct); return results; } - public async Task> GetReadingHistoryItems(StatsFilterDto filter, UserParams userParams, int userId, int requestingUserId) + public async Task> GetReadingHistoryItems(StatsFilterDto filter, + UserParams userParams, int userId, int requestingUserId, CancellationToken ct = default) { - var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId); - var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId); + var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId, ct); + var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId, ct: ct); + if (requestingUser == null) return PagedList.Create([], 0, 0, 0); + var userTimeZone = GetTimeZoneOrUtc(filter.TimeZoneId); var query = context.AppUserReadingSessionActivityData @@ -1711,13 +1698,13 @@ public class StatisticService(ILogger logger, DataContext cont .OrderByDescending(a => a.StartTimeUtc); // Get total count before pagination - var totalCount = await query.CountAsync(); + var totalCount = await query.CountAsync(ct); // Paginate and materialize var items = await query .Skip((userParams.PageNumber - 1) * userParams.PageSize) .Take(userParams.PageSize) - .ToListAsync(); + .ToListAsync(ct); var libraryTypes = items.Select(i => i.LibraryType).Distinct().ToList(); var namingContexts = new Dictionary(); @@ -1814,7 +1801,7 @@ public class StatisticService(ILogger logger, DataContext cont return PagedList.Create(dtos, totalCount, userParams.PageNumber, userParams.PageSize); } - private async Task GetAuthorsCount(HashSet chapterIds) + private async Task GetAuthorsCount(HashSet chapterIds, CancellationToken ct = default) { if (chapterIds.Count == 0) return 0; @@ -1824,7 +1811,7 @@ public class StatisticService(ILogger logger, DataContext cont .Where(cp => cp.Role == PersonRole.Writer && chapterIds.Contains(cp.ChapterId)) .Select(cp => cp.PersonId) .Distinct() - .CountAsync(); + .CountAsync(ct); } var authorIds = new HashSet(); @@ -1834,7 +1821,7 @@ public class StatisticService(ILogger logger, DataContext cont var batchAuthors = await context.ChapterPeople .Where(cp => cp.Role == PersonRole.Writer && batchSet.Contains(cp.ChapterId)) .Select(cp => cp.PersonId) - .ToListAsync(); + .ToListAsync(ct); foreach (var id in batchAuthors) authorIds.Add(id); @@ -1843,7 +1830,7 @@ public class StatisticService(ILogger logger, DataContext cont } private async Task<(int Reviews, int Ratings)> GetReviewsAndRatings( - StatsFilterDto filter, int userId, AppUserSocialPreferences socialPreferences) + StatsFilterDto filter, int userId, AppUserSocialPreferences socialPreferences, CancellationToken ct = default) { var baseQuery = BuildRatingQuery(filter, userId, socialPreferences); @@ -1854,7 +1841,7 @@ public class StatisticService(ILogger logger, DataContext cont Reviews = g.Count(r => r.Review != null && r.Review != ""), Ratings = g.Count(r => r.HasBeenRated) }) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); return counts != null ? (counts.Reviews, counts.Ratings) : (0, 0); } @@ -1879,16 +1866,19 @@ public class StatisticService(ILogger logger, DataContext cont r.Series.Metadata.AgeRating == AgeRating.Unknown)); } - public async Task>> GetReadsPerMonth(StatsFilterDto filter, int userId, int requestingUserId) + public async Task>> GetReadsPerMonth(StatsFilterDto filter, int userId, + int requestingUserId, CancellationToken ct = default) { - var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId); - var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId); + var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId, ct); + var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId, ct: ct); + if (requestingUser == null) return []; + var userTimeZone = GetTimeZoneOrUtc(filter.TimeZoneId); var rawData = await context.AppUserReadingSessionActivityData .ApplyStatsFilter(filter, userId, socialPreferences, requestingUser, isAggregate: true) .Select(s => s.ReadingSession.CreatedUtc) - .ToListAsync(); + .ToListAsync(ct); return rawData .Select(utc => TimeZoneInfo.ConvertTimeFromUtc(utc, userTimeZone)) @@ -1907,10 +1897,12 @@ public class StatisticService(ILogger logger, DataContext cont .ToList(); } - public async Task> GetMostReadAuthors(StatsFilterDto filter, int userId, int requestingUserId) + public async Task> GetMostReadAuthors(StatsFilterDto filter, int userId, + int requestingUserId, CancellationToken ct = default) { - var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId); - var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId); + var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId, ct); + var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId, ct: ct); + if (requestingUser == null) return []; var res = await context.ChapterPeople .Where(cp => cp.Role == PersonRole.Writer) @@ -1930,7 +1922,7 @@ public class StatisticService(ILogger logger, DataContext cont }) .OrderByDescending(x => x.TotalChaptersRead) .Take(5) - .ToListAsync(); + .ToListAsync(ct); var final = new List(); @@ -1942,9 +1934,9 @@ public class StatisticService(ILogger logger, DataContext cont { Chapter = c, SeriesId = c.Volume.Series.Id, - LibraryId = c.Volume.Series.LibraryId, + c.Volume.Series.LibraryId, }) - .ToListAsync(); + .ToListAsync(ct); final.Add(new MostReadAuthorsDto @@ -1957,7 +1949,7 @@ public class StatisticService(ILogger logger, DataContext cont LibraryId = x.LibraryId, SeriesId = x.SeriesId, ChapterId = x.Chapter.Id, - Title = x.Chapter.TitleName, // TODO: Use that method that makes a smart title? Do we have that? Where it falls back to Chapter #3 or whatever + Title = x.Chapter.TitleName, // default: Use that method that makes a smart title? Do we have that? Where it falls back to Chapter #3 or whatever }).ToList(), }); } @@ -1966,12 +1958,13 @@ public class StatisticService(ILogger logger, DataContext cont } - public async Task GetTotalReads(int userId, int requestingUserId) + public async Task GetTotalReads(int userId, int requestingUserId, CancellationToken ct = default) { - var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId); - var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId); + var socialPreferences = await unitOfWork.UserRepository.GetSocialPreferencesForUser(userId, ct); + var requestingUser = await unitOfWork.UserRepository.GetUserByIdAsync(requestingUserId, ct: ct); + if (requestingUser == null) return 0; - var librariesForUser = await unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId); + var librariesForUser = await unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId, ct: ct); var filter = new StatsFilterDto { Libraries = librariesForUser, @@ -1979,14 +1972,14 @@ public class StatisticService(ILogger logger, DataContext cont return await context.AppUserReadingSessionActivityData .ApplyStatsFilter(filter, userId, socialPreferences, requestingUser, isAggregate: true) - .CountAsync(); + .CountAsync(ct); } - public async Task> GetTopUsers(int days) + public async Task> GetTopUsers(int days, CancellationToken ct = default) { - var libraries = (await unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList(); - var users = (await unitOfWork.UserRepository.GetAllUsersAsync()).ToList(); + var libraries = (await unitOfWork.LibraryRepository.GetLibrariesAsync(ct: ct)).ToList(); + var users = (await unitOfWork.UserRepository.GetAllUsersAsync(ct: ct)).ToList(); var minDate = DateTime.Now.Subtract(TimeSpan.FromDays(days)); var topUsersAndReadChapters = context.AppUserProgresses diff --git a/API/Services/Tasks/StatsService.cs b/Kavita.Services/StatsService.cs similarity index 93% rename from API/Services/Tasks/StatsService.cs rename to Kavita.Services/StatsService.cs index 7b725114a..0c503f7d2 100644 --- a/API/Services/Tasks/StatsService.cs +++ b/Kavita.Services/StatsService.cs @@ -5,37 +5,32 @@ using System.Globalization; using System.Linq; using System.Net.Http; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.Data.Misc; -using API.Data.Repositories; -using API.DTOs.Stats; -using API.DTOs.Stats.V3; -using API.Entities; -using API.Entities.Enums; -using API.Extensions; -using API.Services.Plus; -using API.Services.Tasks.Scanner.Parser; using Flurl.Http; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Plus; using Kavita.Common; using Kavita.Common.EnvironmentInfo; +using Kavita.Common.Extensions; using Kavita.Common.Helpers; +using Kavita.Database; +using Kavita.Models.DTOs.Stats; +using Kavita.Models.DTOs.Stats.V3; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Services.Scanner; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace API.Services.Tasks; +namespace Kavita.Services; -#nullable enable - -public interface IStatsService -{ - Task Send(); - Task GetServerInfoSlim(); - Task SendCancellation(); -} /// /// This is for reporting to the stat server /// @@ -72,9 +67,10 @@ public class StatsService : IStatsService /// Due to all instances firing this at the same time, we can DDOS our server. This task when fired will schedule the task to be run /// randomly over a six-hour spread /// - public async Task Send() + /// + public async Task Send(CancellationToken ct = default) { - var allowStatCollection = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).AllowStatCollection; + var allowStatCollection = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct)).AllowStatCollection; if (!allowStatCollection) { return; @@ -87,17 +83,17 @@ public class StatsService : IStatsService /// This must be public for Hangfire. Do not call this directly. ///
// ReSharper disable once MemberCanBePrivate.Global - public async Task SendData() + public async Task SendData(CancellationToken ct = default) { var sw = Stopwatch.StartNew(); var data = await GetStatV3Payload(); _logger.LogDebug("Collecting stats took {Time} ms", sw.ElapsedMilliseconds); sw.Stop(); - await SendDataToStatsServer(data); + await SendDataToStatsServer(data, ct); } - private async Task SendDataToStatsServer(ServerInfoV3Dto data) + private async Task SendDataToStatsServer(ServerInfoV3Dto data, CancellationToken ct = default) { var responseContent = string.Empty; @@ -105,7 +101,7 @@ public class StatsService : IStatsService { var response = await (_apiUrl + "/api/v3/stats") .WithBasicHeaders(ApiKey) - .PostJsonAsync(data); + .PostJsonAsync(data, cancellationToken: ct); if (response.StatusCode != StatusCodes.Status200OK) { @@ -118,7 +114,7 @@ public class StatsService : IStatsService UPDATE ServerSetting SET Value = CAST(CAST(Value AS INTEGER) + 1 AS TEXT) WHERE Key = {ServerSettingKey.StatsApiHits} - """); + """, cancellationToken: ct); } catch (HttpRequestException e) @@ -138,9 +134,9 @@ public class StatsService : IStatsService } - public async Task GetServerInfoSlim() + public async Task GetServerInfoSlim(CancellationToken ct = default) { - var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct); return new ServerInfoSlimDto() { InstallId = serverSettings.InstallId, @@ -151,10 +147,10 @@ public class StatsService : IStatsService }; } - public async Task SendCancellation() + public async Task SendCancellation(CancellationToken ct = default) { _logger.LogInformation("Informing KavitaStats that this instance is no longer sending stats"); - var installId = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).InstallId; + var installId = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct)).InstallId; var responseContent = string.Empty; @@ -163,7 +159,7 @@ public class StatsService : IStatsService var response = await (_apiUrl + "/api/v2/stats/opt-out?installId=" + installId) .WithBasicHeaders(ApiKey) .WithTimeout(TimeSpan.FromSeconds(30)) - .PostAsync(); + .PostAsync(cancellationToken: ct); if (response.StatusCode != StatusCodes.Status200OK) { diff --git a/Kavita.Services/StreamService.cs b/Kavita.Services/StreamService.cs new file mode 100644 index 000000000..d85858cae --- /dev/null +++ b/Kavita.Services/StreamService.cs @@ -0,0 +1,415 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; +using Kavita.Common; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Dashboard; +using Kavita.Models.DTOs.SideNav; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.User; +using Kavita.Models.Helpers; +using Microsoft.Extensions.Logging; + +namespace Kavita.Services; + +public class StreamService( + IUnitOfWork unitOfWork, + IEventHub eventHub, + ILocalizationService localizationService, + ILogger logger) + : IStreamService +{ + public async Task> GetDashboardStreams(int userId, bool visibleOnly = true, + CancellationToken ct = default) + { + return await unitOfWork.UserRepository.GetDashboardStreams(userId, visibleOnly, ct); + } + + public async Task> GetSidenavStreams(int userId, bool visibleOnly = true, CancellationToken ct = default) + { + return await unitOfWork.UserRepository.GetSideNavStreams(userId, visibleOnly, ct); + } + + public async Task> GetExternalSources(int userId, CancellationToken ct = default) + { + return await unitOfWork.AppUserExternalSourceRepository.GetExternalSources(userId, ct); + } + + public async Task CreateDashboardStreamFromSmartFilter(int userId, int smartFilterId, + CancellationToken ct = default) + { + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.DashboardStreams, ct); + if (user == null) throw new KavitaException(await localizationService.Translate(userId, "no-user")); + + var smartFilter = await unitOfWork.AppUserSmartFilterRepository.GetById(smartFilterId, ct); + if (smartFilter == null) throw new KavitaException(await localizationService.Translate(userId, "smart-filter-doesnt-exist")); + + var stream = user.DashboardStreams.FirstOrDefault(d => d.SmartFilter?.Id == smartFilterId); + if (stream != null) throw new KavitaException(await localizationService.Translate(userId, "smart-filter-already-in-use")); + + var maxOrder = user!.DashboardStreams.Max(d => d.Order); + var createdStream = new AppUserDashboardStream() + { + Name = smartFilter.Name, + IsProvided = false, + StreamType = DashboardStreamType.SmartFilter, + Visible = true, + Order = maxOrder + 1, + SmartFilter = smartFilter + }; + + user.DashboardStreams.Add(createdStream); + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(ct); + + var ret = new DashboardStreamDto() + { + Id = createdStream.Id, + Name = createdStream.Name, + IsProvided = createdStream.IsProvided, + Visible = createdStream.Visible, + Order = createdStream.Order, + SmartFilterEncoded = smartFilter.Filter, + StreamType = createdStream.StreamType + }; + + await eventHub.SendMessageToAsync(MessageFactory.DashboardUpdate, MessageFactory.DashboardUpdateEvent(user.Id), + userId, ct); + + return ret; + } + + public async Task UpdateDashboardStream(int userId, DashboardStreamDto dto, CancellationToken ct = default) + { + var stream = await unitOfWork.UserRepository.GetDashboardStream(dto.Id, ct); + if (stream == null) throw new KavitaException(await localizationService.Translate(userId, "dashboard-stream-doesnt-exist")); + stream.Visible = dto.Visible; + + unitOfWork.UserRepository.Update(stream); + await unitOfWork.CommitAsync(ct); + await eventHub.SendMessageToAsync(MessageFactory.DashboardUpdate, MessageFactory.DashboardUpdateEvent(userId), + userId, ct); + } + + public async Task UpdateDashboardStreamPosition(int userId, UpdateStreamPositionDto dto, + CancellationToken ct = default) + { + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, + AppUserIncludes.DashboardStreams, ct); + var stream = user?.DashboardStreams.FirstOrDefault(d => d.Id == dto.Id); + if (stream == null) + { + throw new KavitaException(await localizationService.Translate(userId, "dashboard-stream-doesnt-exist")); + } + + if (stream.Order == dto.ToPosition) return; + + var list = user!.DashboardStreams.OrderBy(s => s.Order).ToList(); + OrderableHelper.ReorderItems(list, stream.Id, dto.ToPosition); + user.DashboardStreams = list; + + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(ct); + if (!stream.Visible) return; + await eventHub.SendMessageToAsync(MessageFactory.DashboardUpdate, MessageFactory.DashboardUpdateEvent(user.Id), + user.Id, ct); + } + + public async Task UpdateSideNavStreamBulk(int userId, BulkUpdateSideNavStreamVisibilityDto dto, + CancellationToken ct = default) + { + var streams = await unitOfWork.UserRepository.GetDashboardStreamsByIds(dto.Ids, ct); + foreach (var stream in streams) + { + stream.Visible = dto.Visibility; + unitOfWork.UserRepository.Update(stream); + } + + await unitOfWork.CommitAsync(ct); + await eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId), + userId, ct); + } + + public async Task CreateSideNavStreamFromSmartFilter(int userId, int smartFilterId, + CancellationToken ct = default) + { + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.SideNavStreams, ct); + if (user == null) throw new KavitaException(await localizationService.Translate(userId, "no-user")); + + var smartFilter = await unitOfWork.AppUserSmartFilterRepository.GetById(smartFilterId, ct); + if (smartFilter == null) throw new KavitaException(await localizationService.Translate(userId, "smart-filter-doesnt-exist")); + + var stream = user.SideNavStreams.FirstOrDefault(d => d.SmartFilter?.Id == smartFilterId); + if (stream != null) throw new KavitaException(await localizationService.Translate(userId, "smart-filter-already-in-use")); + + var maxOrder = user!.SideNavStreams.Max(d => d.Order); + var createdStream = new AppUserSideNavStream() + { + Name = smartFilter.Name, + IsProvided = false, + StreamType = SideNavStreamType.SmartFilter, + Visible = true, + Order = maxOrder + 1, + SmartFilter = smartFilter + }; + + user.SideNavStreams.Add(createdStream); + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(ct); + + var ret = new SideNavStreamDto() + { + Id = createdStream.Id, + Name = createdStream.Name, + IsProvided = createdStream.IsProvided, + Visible = createdStream.Visible, + Order = createdStream.Order, + SmartFilterEncoded = smartFilter.Filter, + StreamType = createdStream.StreamType + }; + + + await eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId), + userId, ct); + return ret; + } + + public async Task CreateSideNavStreamFromExternalSource(int userId, int externalSourceId, + CancellationToken ct = default) + { + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.SideNavStreams, ct); + if (user == null) throw new KavitaException(await localizationService.Translate(userId, "no-user")); + + var externalSource = await unitOfWork.AppUserExternalSourceRepository.GetById(externalSourceId, ct); + if (externalSource == null) throw new KavitaException(await localizationService.Translate(userId, "external-source-doesnt-exist")); + + var stream = user?.SideNavStreams.FirstOrDefault(d => d.ExternalSourceId == externalSourceId); + if (stream != null) throw new KavitaException(await localizationService.Translate(userId, "external-source-already-in-use")); + + var maxOrder = user!.SideNavStreams.Max(d => d.Order); + var createdStream = new AppUserSideNavStream() + { + Name = externalSource.Name, + IsProvided = false, + StreamType = SideNavStreamType.ExternalSource, + Visible = true, + Order = maxOrder + 1, + ExternalSourceId = externalSource.Id + }; + + user.SideNavStreams.Add(createdStream); + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(ct); + + var ret = new SideNavStreamDto() + { + Name = createdStream.Name, + IsProvided = createdStream.IsProvided, + Visible = createdStream.Visible, + Order = createdStream.Order, + StreamType = createdStream.StreamType, + ExternalSource = new ExternalSourceDto() + { + Host = externalSource.Host, + Id = externalSource.Id, + Name = externalSource.Name, + ApiKey = externalSource.ApiKey + } + }; + + + await eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId), + userId, ct); + return ret; + } + + public async Task UpdateSideNavStream(int userId, SideNavStreamDto dto, CancellationToken ct = default) + { + var stream = await unitOfWork.UserRepository.GetSideNavStream(dto.Id, ct); + if (stream == null) + throw new KavitaException(await localizationService.Translate(userId, "sidenav-stream-doesnt-exist")); + + stream.Visible = dto.Visible; + + unitOfWork.UserRepository.Update(stream); + await unitOfWork.CommitAsync(ct); + await eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId), + userId, ct); + } + + public async Task UpdateSideNavStreamPosition(int userId, UpdateStreamPositionDto dto, CancellationToken ct = default) + { + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, + AppUserIncludes.SideNavStreams, ct); + var stream = user?.SideNavStreams.FirstOrDefault(d => d.Id == dto.Id); + if (stream == null) throw new KavitaException(await localizationService.Translate(userId, "sidenav-stream-doesnt-exist")); + + if (stream.Order == dto.ToPosition) return; + + var list = user!.SideNavStreams.OrderBy(s => s.Order).ToList(); + + var wantedPosition = dto.ToPosition; + if (!dto.PositionIncludesInvisible) + { + var visibleItems = list.Where(i => i.Visible).ToList(); + if (dto.ToPosition < 0 || dto.ToPosition >= visibleItems.Count) return; + + var itemAtWantedPosition = visibleItems[dto.ToPosition]; + wantedPosition = list.IndexOf(itemAtWantedPosition); + } + + OrderableHelper.ReorderItems(list, stream.Id, wantedPosition); + user.SideNavStreams = list; + + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(ct); + if (!stream.Visible) return; + await eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(userId), + userId, ct); + } + + public async Task CreateExternalSource(int userId, ExternalSourceDto dto, + CancellationToken ct = default) + { + var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, + AppUserIncludes.ExternalSources, ct); + if (user == null) throw new KavitaException("not-authenticated"); + + if (user.ExternalSources.Any(s => s.Host == dto.Host)) + { + throw new KavitaException("external-source-already-exists"); + } + + if (string.IsNullOrEmpty(dto.ApiKey) || string.IsNullOrEmpty(dto.Name)) throw new KavitaException("external-source-required"); + if (!UrlHelper.StartsWithHttpOrHttps(dto.Host)) throw new KavitaException("external-source-host-format"); + + + var newSource = new AppUserExternalSource() + { + Name = dto.Name, + Host = UrlHelper.EnsureEndsWithSlash( + UrlHelper.EnsureStartsWithHttpOrHttps(dto.Host)), + ApiKey = dto.ApiKey + }; + user.ExternalSources.Add(newSource); + + unitOfWork.UserRepository.Update(user); + await unitOfWork.CommitAsync(ct); + + dto.Id = newSource.Id; + + return dto; + } + + public async Task UpdateExternalSource(int userId, ExternalSourceDto dto, + CancellationToken ct = default) + { + var source = await unitOfWork.AppUserExternalSourceRepository.GetById(dto.Id, ct); + if (source == null) throw new KavitaException("external-source-doesnt-exist"); + if (source.AppUserId != userId) throw new KavitaException("external-source-doesnt-exist"); + + if (string.IsNullOrEmpty(dto.ApiKey) || string.IsNullOrEmpty(dto.Host) || string.IsNullOrEmpty(dto.Name)) throw new KavitaException("external-source-required"); + + source.Host = UrlHelper.EnsureEndsWithSlash( + UrlHelper.EnsureStartsWithHttpOrHttps(dto.Host)); + source.ApiKey = dto.ApiKey; + source.Name = dto.Name; + + unitOfWork.AppUserExternalSourceRepository.Update(source); + await unitOfWork.CommitAsync(ct); + + dto.Host = source.Host; + return dto; + } + + public async Task DeleteExternalSource(int userId, int externalSourceId, CancellationToken ct = default) + { + var source = await unitOfWork.AppUserExternalSourceRepository.GetById(externalSourceId, ct); + if (source == null) throw new KavitaException("external-source-doesnt-exist"); + if (source.AppUserId != userId) throw new KavitaException("external-source-doesnt-exist"); + + unitOfWork.AppUserExternalSourceRepository.Delete(source); + + // Find all SideNav's with this source and delete them as well + var streams2 = await unitOfWork.UserRepository.GetSideNavStreamWithExternalSource(externalSourceId, ct); + unitOfWork.UserRepository.Delete(streams2); + + await unitOfWork.CommitAsync(ct); + } + + public async Task DeleteSideNavSmartFilterStream(int userId, int sideNavStreamId, CancellationToken ct = default) + { + try + { + var stream = await unitOfWork.UserRepository.GetSideNavStream(sideNavStreamId, ct); + if (stream == null) throw new KavitaException("sidenav-stream-doesnt-exist"); + + if (stream.AppUserId != userId) throw new KavitaException("sidenav-stream-doesnt-exist"); + + + if (stream.StreamType != SideNavStreamType.SmartFilter) + { + throw new KavitaException("sidenav-stream-only-delete-smart-filter"); + } + + unitOfWork.UserRepository.Delete(stream); + + await unitOfWork.CommitAsync(ct); + } + catch (Exception ex) + { + logger.LogError(ex, "There was an exception deleting SideNav Smart Filter Stream: {FilterId}", sideNavStreamId); + throw; + } + } + + public async Task DeleteDashboardSmartFilterStream(int userId, int dashboardStreamId, CancellationToken ct = default) + { + try + { + var stream = await unitOfWork.UserRepository.GetDashboardStream(dashboardStreamId, ct); + if (stream == null) throw new KavitaException("dashboard-stream-doesnt-exist"); + + if (stream.AppUserId != userId) throw new KavitaException("dashboard-stream-doesnt-exist"); + + if (stream.StreamType != DashboardStreamType.SmartFilter) + { + throw new KavitaException("dashboard-stream-only-delete-smart-filter"); + } + + unitOfWork.UserRepository.Delete(stream); + + await unitOfWork.CommitAsync(ct); + } catch (Exception ex) + { + logger.LogError(ex, "There was an exception deleting Dashboard Smart Filter Stream: {FilterId}", dashboardStreamId); + throw; + } + } + + public async Task RenameSmartFilterStreams(AppUserSmartFilter smartFilter, CancellationToken ct = default) + { + var sideNavStreams = await unitOfWork.UserRepository.GetSideNavStreamWithFilter(smartFilter.Id, ct); + var dashboardStreams = await unitOfWork.UserRepository.GetDashboardStreamWithFilter(smartFilter.Id, ct); + + foreach (var sideNavStream in sideNavStreams) + { + sideNavStream.Name = smartFilter.Name; + } + + foreach (var dashboardStream in dashboardStreams) + { + dashboardStream.Name = smartFilter.Name; + } + + await unitOfWork.CommitAsync(ct); + } +} diff --git a/API/Services/TachiyomiService.cs b/Kavita.Services/TachiyomiService.cs similarity index 52% rename from API/Services/TachiyomiService.cs rename to Kavita.Services/TachiyomiService.cs index e5e733bef..f6d810946 100644 --- a/API/Services/TachiyomiService.cs +++ b/Kavita.Services/TachiyomiService.cs @@ -1,81 +1,60 @@ using System; -using API.DTOs; using System.Threading.Tasks; -using API.Data; using System.Collections.Immutable; using System.Collections.Generic; using System.Globalization; using System.Linq; -using API.Comparators; -using API.Entities; -using API.Entities.Progress; -using API.Extensions; -using API.Services.Reading; -using API.Services.Tasks.Scanner.Parser; +using System.Threading; using AutoMapper; +using Kavita.API.Database; +using Kavita.API.Services; +using Kavita.API.Services.Reading; +using Kavita.Common.Extensions; +using Kavita.Models.DTOs; +using Kavita.Models.Entities.Progress; +using Kavita.Models.Entities.User; +using Kavita.Services.Comparators; +using Kavita.Services.Extensions; +using Kavita.Services.Scanner; using Microsoft.Extensions.Logging; -namespace API.Services; -#nullable enable - -public interface ITachiyomiService -{ - Task GetLatestChapter(int seriesId, int userId); - Task MarkChaptersUntilAsRead(AppUser userWithProgress, int seriesId, float chapterNumber); -} +namespace Kavita.Services; /// /// All APIs are for Tachiyomi extension and app. They have hacks for our implementation and should not be used for any /// other purposes. /// -public class TachiyomiService : ITachiyomiService +public class TachiyomiService( + IUnitOfWork unitOfWork, + IMapper mapper, + ILogger logger, + IReaderService readerService) + : ITachiyomiService { - private readonly IUnitOfWork _unitOfWork; - private readonly IMapper _mapper; - private readonly ILogger _logger; - private readonly IReaderService _readerService; - private static readonly CultureInfo EnglishCulture = CultureInfo.CreateSpecificCulture("en-US"); - public TachiyomiService(IUnitOfWork unitOfWork, IMapper mapper, ILogger logger, IReaderService readerService) + public async Task GetLatestChapter(int seriesId, int userId, CancellationToken ct = default) { - _unitOfWork = unitOfWork; - _readerService = readerService; - _mapper = mapper; - _logger = logger; - } - - /// - /// Gets the latest chapter/volume read. - /// - /// - /// - /// Due to how Tachiyomi works we need a hack to properly return both chapters and volumes. - /// If its a chapter, return the chapterDto as is. - /// If it's a volume, the volume number gets returned in the 'Number' attribute of a chapterDto encoded. - /// The volume number gets divided by 10,000 because that's how Tachiyomi interprets volumes - public async Task GetLatestChapter(int seriesId, int userId) - { - var currentChapter = await _readerService.GetContinuePoint(seriesId, userId); + var currentChapter = await readerService.GetContinuePoint(seriesId, userId); var prevChapterId = - await _readerService.GetPrevChapterIdAsync(seriesId, currentChapter.VolumeId, currentChapter.Id, userId); + await readerService.GetPrevChapterIdAsync(seriesId, currentChapter.VolumeId, currentChapter.Id, userId); // If prevChapterId is -1, this means either nothing is read or everything is read. if (prevChapterId == -1) { - var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); + var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId, ct); var userHasProgress = series.PagesRead != 0 && series.PagesRead <= series.Pages; // If the user doesn't have progress, then return null, which the extension will catch as 204 (no content) and report nothing as read if (!userHasProgress) return null; // Else return the max chapter to Tachiyomi so it can consider everything read - var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(seriesId)).ToImmutableList(); + var volumes = (await unitOfWork.VolumeRepository.GetVolumes(seriesId, ct)).ToImmutableList(); var looseLeafChapterVolume = volumes.GetLooseLeafVolumeOrDefault(); if (looseLeafChapterVolume == null) { - var volumeChapter = _mapper.Map(volumes + var volumeChapter = mapper.Map(volumes [^1].Chapters .OrderBy(c => c.MinNumber, ChapterSortComparerDefaultFirst.Default) .Last()); @@ -93,13 +72,13 @@ public class TachiyomiService : ITachiyomiService .OrderBy(c => c.MinNumber, ChapterSortComparerDefaultLast.Default) .Last(); - return _mapper.Map(lastChapter); + return mapper.Map(lastChapter); } // There is progress, we now need to figure out the highest volume or chapter and return that. - var prevChapter = (await _unitOfWork.ChapterRepository.GetChapterDtoAsync(prevChapterId, userId))!; + var prevChapter = (await unitOfWork.ChapterRepository.GetChapterDtoAsync(prevChapterId, userId, ct))!; - var volumeWithProgress = (await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(prevChapter.VolumeId, userId))!; + var volumeWithProgress = (await unitOfWork.VolumeRepository.GetVolumeDtoAsync(prevChapter.VolumeId, userId, ct))!; // We only encode for single-file volumes if (!volumeWithProgress.IsLooseLeaf() && volumeWithProgress.Chapters.Count == 1) { @@ -108,7 +87,7 @@ public class TachiyomiService : ITachiyomiService } // Progress is just on a chapter, return as is - return _mapper.Map(prevChapter); + return mapper.Map(prevChapter); } private static TachiyomiChapterDto CreateTachiyomiChapterDto(float number) @@ -121,16 +100,10 @@ public class TachiyomiService : ITachiyomiService }; } - /// - /// Marks every chapter and volume that is sorted below the passed number as Read. This will not mark any specials as read. - /// Passed number will also be marked as read - /// - /// - /// - /// Can also be a Tachiyomi encoded volume number - public async Task MarkChaptersUntilAsRead(AppUser userWithProgress, int seriesId, float chapterNumber) + public async Task MarkChaptersUntilAsRead(AppUser userWithProgress, int seriesId, float chapterNumber, + CancellationToken ct = default) { - userWithProgress.Progresses ??= new List(); + userWithProgress.Progresses ??= []; switch (chapterNumber) { @@ -143,22 +116,22 @@ public class TachiyomiService : ITachiyomiService { // This is a hack to track volume number. We need to map it back by x10,000 var volumeNumber = int.Parse($"{(int)(chapterNumber * 10_000)}", EnglishCulture); - await _readerService.MarkVolumesUntilAsRead(userWithProgress, seriesId, volumeNumber); + await readerService.MarkVolumesUntilAsRead(userWithProgress, seriesId, volumeNumber); break; } default: - await _readerService.MarkChaptersUntilAsRead(userWithProgress, seriesId, chapterNumber); + await readerService.MarkChaptersUntilAsRead(userWithProgress, seriesId, chapterNumber); break; } try { - _unitOfWork.UserRepository.Update(userWithProgress); + unitOfWork.UserRepository.Update(userWithProgress); - if (!_unitOfWork.HasChanges()) return true; - if (await _unitOfWork.CommitAsync()) return true; + if (!unitOfWork.HasChanges()) return true; + if (await unitOfWork.CommitAsync(ct)) return true; } catch (Exception ex) { - _logger.LogError(ex, "There was an error saving progress from tachiyomi"); - await _unitOfWork.RollbackAsync(); + logger.LogError(ex, "There was an error saving progress from tachiyomi"); + await unitOfWork.RollbackAsync(ct); } return false; } diff --git a/API/Services/TaskScheduler.cs b/Kavita.Services/TaskScheduler.cs similarity index 81% rename from API/Services/TaskScheduler.cs rename to Kavita.Services/TaskScheduler.cs index 5964f8c3a..9a9c36fe9 100644 --- a/API/Services/TaskScheduler.cs +++ b/Kavita.Services/TaskScheduler.cs @@ -2,51 +2,33 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using API.Data; -using API.Data.Repositories; -using API.Entities.Enums; -using API.Entities.Enums.User; -using API.Extensions; -using API.Helpers; -using API.Helpers.Converters; -using API.Services.Caching; -using API.Services.Plus; -using API.Services.Reading; -using API.Services.Tasks; -using API.Services.Tasks.Metadata; -using API.SignalR; using Hangfire; +using Kavita.API.Database; +using Kavita.API.Repositories; +using Kavita.API.Services; +using Kavita.API.Services.Metadata; +using Kavita.API.Services.Plus; +using Kavita.API.Services.Reading; +using Kavita.API.Services.Scanner; +using Kavita.API.Services.SignalR; +using Kavita.Common.Constants; using Kavita.Common.Helpers; +using Kavita.Models.Constants; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.Entities.Enums; +using Kavita.Models.Entities.Enums.User; +using Kavita.Models.Extensions; +using Kavita.Services.Plus; +using Kavita.Services.Scanner; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Polly; using Polly.Retry; -namespace API.Services; +namespace Kavita.Services; -public interface ITaskScheduler -{ - Task ScheduleTasks(); - Task ScheduleStatsTasks(); - void ScheduleUpdaterTasks(); - Task ScheduleKavitaPlusTasks(); - void ScanFolder(string folderPath, string originalPath, TimeSpan delay); - void ScanFolder(string folderPath, bool abortOnNoSeriesMatch = false); - Task ScanLibrary(int libraryId, bool force = false); - Task ScanLibraries(bool force = false); - void CleanupChapters(int[] chapterIds); - void RefreshMetadata(int libraryId, bool forceUpdate = true, bool forceColorscape = true); - Task RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false, bool forceColorscape = false); - Task ScanSeries(int libraryId, int seriesId, bool forceUpdate = false); - void AnalyzeFilesForSeries(int libraryId, int seriesId, bool forceUpdate = false); - void CancelStatsTasks(); - Task RunStatCollection(); - void CovertAllCoversToEncoding(); - Task CleanupDbEntries(); - Task CheckForUpdate(); - Task SyncThemes(); -} public class TaskScheduler : ITaskScheduler { private readonly ICacheService _cacheService; @@ -59,7 +41,6 @@ public class TaskScheduler : ITaskScheduler private readonly IStatsService _statsService; private readonly IVersionUpdaterService _versionUpdaterService; - private readonly IThemeService _themeService; private readonly IWordCountAnalyzerService _wordCountAnalyzerService; private readonly IStatisticService _statisticService; private readonly IMediaConversionService _mediaConversionService; @@ -70,30 +51,30 @@ public class TaskScheduler : ITaskScheduler private readonly IWantToReadSyncService _wantToReadSyncService; private readonly IEventHub _eventHub; private readonly IEmailService _emailService; - private readonly IAuthKeyCacheInvalidator _authKeyCacheInvalidator; + private readonly IAuthKeyService _authKeyService; public static BackgroundJobServer Client => new (); - public const string ScanQueue = "scan"; - public const string DefaultQueue = "default"; - public const string RemoveFromWantToReadTaskId = "remove-from-want-to-read"; - public const string UpdateYearlyStatsTaskId = "update-yearly-stats"; - public const string SyncThemesTaskId = "sync-themes"; - public const string CheckForUpdateId = "check-updates"; - public const string CleanupDbTaskId = "cleanup-db"; - public const string CleanupTaskId = "cleanup"; - public const string BackupTaskId = "backup"; - public const string ScanLibrariesTaskId = "scan-libraries"; - public const string ReportStatsTaskId = "report-stats"; - 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"; - public const string KavitaPlusStackSyncId = "kavita+-stack-sync"; - public const string KavitaPlusWantToReadSyncId = "kavita+-want-to-read-sync"; - public const string ReadingHistoryAggregationId = "reading-history-aggregation"; - public const string AuthKeyExpirationId = "auth-key-expiration"; - public const string EnsureSideNavId = "ensure-sidenav"; + public const string ScanQueue = TaskSchedulerConstants.ScanQueue; + public const string DefaultQueue = TaskSchedulerConstants.DefaultQueue; + public const string RemoveFromWantToReadTaskId = TaskSchedulerConstants.RemoveFromWantToReadTaskId; + public const string UpdateYearlyStatsTaskId = TaskSchedulerConstants.UpdateYearlyStatsTaskId; + public const string SyncThemesTaskId = TaskSchedulerConstants.SyncThemesTaskId; + public const string CheckForUpdateId = TaskSchedulerConstants.CheckForUpdateId; + public const string CleanupDbTaskId = TaskSchedulerConstants.CleanupDbTaskId; + public const string CleanupTaskId = TaskSchedulerConstants.CleanupTaskId; + public const string BackupTaskId = TaskSchedulerConstants.BackupTaskId; + public const string ScanLibrariesTaskId = TaskSchedulerConstants.ScanLibrariesTaskId; + public const string ReportStatsTaskId = TaskSchedulerConstants.ReportStatsTaskId; + public const string CheckScrobblingTokensId = TaskSchedulerConstants.CheckScrobblingTokensId; + public const string ProcessScrobblingEventsId = TaskSchedulerConstants.ProcessScrobblingEventsId; + public const string ProcessProcessedScrobblingEventsId = TaskSchedulerConstants.ProcessProcessedScrobblingEventsId; + public const string LicenseCheckId = TaskSchedulerConstants.LicenseCheckId; + public const string KavitaPlusDataRefreshId = TaskSchedulerConstants.KavitaPlusDataRefreshId; + public const string KavitaPlusStackSyncId = TaskSchedulerConstants.KavitaPlusStackSyncId; + public const string KavitaPlusWantToReadSyncId = TaskSchedulerConstants.KavitaPlusWantToReadSyncId; + public const string ReadingHistoryAggregationId = TaskSchedulerConstants.ReadingHistoryAggregationId; + public const string AuthKeyExpirationId = TaskSchedulerConstants.AuthKeyExpirationId; + public const string EnsureSideNavId = TaskSchedulerConstants.EnsureSideNavId; private const int BaseRetryDelay = 60; // 1-minute @@ -116,11 +97,11 @@ public class TaskScheduler : ITaskScheduler public TaskScheduler(ICacheService cacheService, ILogger logger, IScannerService scannerService, IUnitOfWork unitOfWork, IMetadataService metadataService, IBackupService backupService, ICleanupService cleanupService, IStatsService statsService, IVersionUpdaterService versionUpdaterService, - IThemeService themeService, IWordCountAnalyzerService wordCountAnalyzerService, IStatisticService statisticService, + IWordCountAnalyzerService wordCountAnalyzerService, IStatisticService statisticService, IMediaConversionService mediaConversionService, IScrobblingService scrobblingService, ILicenseService licenseService, IExternalMetadataService externalMetadataService, ISmartCollectionSyncService smartCollectionSyncService, IWantToReadSyncService wantToReadSyncService, IEventHub eventHub, IEmailService emailService, - IAuthKeyCacheInvalidator authKeyCacheInvalidator) + IAuthKeyService authKeyService) { _cacheService = cacheService; _logger = logger; @@ -131,7 +112,6 @@ public class TaskScheduler : ITaskScheduler _cleanupService = cleanupService; _statsService = statsService; _versionUpdaterService = versionUpdaterService; - _themeService = themeService; _wordCountAnalyzerService = wordCountAnalyzerService; _statisticService = statisticService; _mediaConversionService = mediaConversionService; @@ -142,7 +122,7 @@ public class TaskScheduler : ITaskScheduler _wantToReadSyncService = wantToReadSyncService; _eventHub = eventHub; _emailService = emailService; - _authKeyCacheInvalidator = authKeyCacheInvalidator; + _authKeyService = authKeyService; _defaultRetryPolicy = Policy .Handle() @@ -163,12 +143,12 @@ public class TaskScheduler : ITaskScheduler ); } - public async Task ScheduleTasks() + public async Task ScheduleTasks(CancellationToken cancellationToken = default) { _logger.LogInformation("Scheduling reoccurring tasks"); - var setting = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskScan)).Value; + var setting = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskScan, cancellationToken)).Value; if (IsInvalidCronSetting(setting)) { _logger.LogError("Scan Task has invalid cron, defaulting to Daily"); @@ -184,11 +164,11 @@ public class TaskScheduler : ITaskScheduler } - setting = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskBackup)).Value; + setting = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskBackup, cancellationToken)).Value; if (IsInvalidCronSetting(setting)) { _logger.LogError("Backup Task has invalid cron, defaulting to Weekly"); - RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(), + RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(CancellationToken.None), Cron.Weekly, RecurringJobOptions); } else @@ -200,43 +180,48 @@ 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(), + RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(CancellationToken.None), () => schedule, RecurringJobOptions); } - setting = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskCleanup)).Value; + setting = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskCleanup, cancellationToken)).Value; if (IsInvalidCronSetting(setting)) { _logger.LogError("Cleanup Task has invalid cron, defaulting to Daily"); - RecurringJob.AddOrUpdate(CleanupTaskId, () => _cleanupService.Cleanup(), + RecurringJob.AddOrUpdate(CleanupTaskId, () => _cleanupService.Cleanup(CancellationToken.None), Cron.Daily, RecurringJobOptions); } else { _logger.LogDebug("Scheduling Cleanup Task for {Setting}", setting); - RecurringJob.AddOrUpdate(CleanupTaskId, () => _cleanupService.Cleanup(), + RecurringJob.AddOrUpdate(CleanupTaskId, () => _cleanupService.Cleanup(CancellationToken.None), CronConverter.ConvertToCronNotation(setting), RecurringJobOptions); } - RecurringJob.AddOrUpdate(RemoveFromWantToReadTaskId, () => _cleanupService.CleanupWantToRead(), + RecurringJob.AddOrUpdate(RemoveFromWantToReadTaskId, + () => _cleanupService.CleanupWantToRead(CancellationToken.None), Cron.Daily, RecurringJobOptions); - RecurringJob.AddOrUpdate(UpdateYearlyStatsTaskId, () => _statisticService.UpdateServerStatistics(), + RecurringJob.AddOrUpdate(UpdateYearlyStatsTaskId, + () => _statisticService.UpdateServerStatistics(CancellationToken.None), Cron.Monthly, RecurringJobOptions); - RecurringJob.AddOrUpdate(SyncThemesTaskId, () => SyncThemes(), + RecurringJob.AddOrUpdate(SyncThemesTaskId, + themeService => themeService.SyncThemes(CancellationToken.None), Cron.Daily, RecurringJobOptions); - RecurringJob.AddOrUpdate(AuthKeyExpirationId, () => CheckExpiredOrExpiringAuthKeys(), + RecurringJob.AddOrUpdate(AuthKeyExpirationId, () => CheckExpiredOrExpiringAuthKeys(CancellationToken.None), Cron.Daily, RecurringJobOptions); - RecurringJob.AddOrUpdate(EnsureSideNavId, () => EnsureSideNav(), Cron.Daily(1), RecurringJobOptions); + RecurringJob.AddOrUpdate(EnsureSideNavId, () => EnsureSideNav(CancellationToken.None), + Cron.Daily(1), RecurringJobOptions); - RecurringJob.AddOrUpdate(ReadingHistoryAggregationId, service => service.AggregateYesterdaysActivity(), + RecurringJob.AddOrUpdate(ReadingHistoryAggregationId, + service => service.AggregateYesterdaysActivity(CancellationToken.None), "5 0 * * *", RecurringJobOptions); // 12:05 AM daily - await ScheduleKavitaPlusTasks(); + await ScheduleKavitaPlusTasks(cancellationToken); } private static bool IsInvalidCronSetting(string setting) @@ -244,46 +229,50 @@ public class TaskScheduler : ITaskScheduler return setting == null || (!NonCronOptions.Contains(setting) && !CronHelper.IsValidCron(setting)); } - public async Task ScheduleKavitaPlusTasks() + public async Task ScheduleKavitaPlusTasks(CancellationToken cancellationToken = default) { // KavitaPlus based (needs license check) - var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; - if (string.IsNullOrEmpty(license) || !await _licenseService.HasActiveSubscription(license)) // TODO: Need to convert this to a non-blocking request + var license = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey, cancellationToken)).Value; + if (string.IsNullOrEmpty(license) || !await _licenseService.HasActiveSubscription(license, cancellationToken)) // TODO: Need to convert this to a non-blocking request { return; } - RecurringJob.AddOrUpdate(CheckScrobblingTokensId, () => _scrobblingService.CheckExternalAccessTokens(), + RecurringJob.AddOrUpdate(CheckScrobblingTokensId, + () => _scrobblingService.CheckExternalAccessTokens(CancellationToken.None), Cron.Daily, RecurringJobOptions); - BackgroundJob.Enqueue(() => _scrobblingService.CheckExternalAccessTokens()); // We also kick off an immediate check on startup + // We also kick off an immediate check on startup + BackgroundJob.Enqueue(() => _scrobblingService.CheckExternalAccessTokens(CancellationToken.None)); // Get the License Info (and cache it) on first load. This will internally cache the Github releases for the Version Service - BackgroundJob.Enqueue(() => _licenseService.GetLicenseInfo(true)); // Kick this off first to cache it then let it refresh every 9 hours (8 hour cache) - RecurringJob.AddOrUpdate(LicenseCheckId, () => _licenseService.GetLicenseInfo(false), + BackgroundJob.Enqueue(() => _licenseService.GetLicenseInfo(true, cancellationToken)); // Kick this off first to cache it then let it refresh every 9 hours (8 hour cache) + RecurringJob.AddOrUpdate(LicenseCheckId, () => _licenseService.GetLicenseInfo(false, cancellationToken), LicenseService.Cron, RecurringJobOptions); // KavitaPlus Scrobbling (every hour) - randomise minutes to spread requests out for K+ var randomMinute = Rnd.Next(0, 60); - RecurringJob.AddOrUpdate(ProcessScrobblingEventsId, () => _scrobblingService.ProcessUpdatesSinceLastSync(), + RecurringJob.AddOrUpdate(ProcessScrobblingEventsId, + () => _scrobblingService.ProcessUpdatesSinceLastSync(CancellationToken.None), Cron.Hourly(randomMinute), RecurringJobOptions); - RecurringJob.AddOrUpdate(ProcessProcessedScrobblingEventsId, () => _scrobblingService.ClearProcessedEvents(), + RecurringJob.AddOrUpdate(ProcessProcessedScrobblingEventsId, + () => _scrobblingService.ClearProcessedEvents(CancellationToken.None), Cron.Daily, RecurringJobOptions); // Backfilling/Freshening Reviews/Rating/Recommendations var randomKPlusBackfill = Rnd.Next(1, 5); RecurringJob.AddOrUpdate(KavitaPlusDataRefreshId, - () => _externalMetadataService.FetchExternalDataTask(), Cron.Daily(randomKPlusBackfill), - RecurringJobOptions); + () => _externalMetadataService.FetchExternalDataTask(CancellationToken.None), + Cron.Daily(randomKPlusBackfill), RecurringJobOptions); // This shouldn't be so close to fetching data due to Rate limit concerns var randomKPlusStackSync = Rnd.Next(6, 10); RecurringJob.AddOrUpdate(KavitaPlusStackSyncId, - () => _smartCollectionSyncService.Sync(), Cron.Daily(randomKPlusStackSync), - RecurringJobOptions); + () => _smartCollectionSyncService.Sync(CancellationToken.None), + Cron.Daily(randomKPlusStackSync), RecurringJobOptions); RecurringJob.AddOrUpdate(KavitaPlusWantToReadSyncId, - () => _wantToReadSyncService.Sync(), Cron.Weekly(DayOfWeekHelper.Random()), - RecurringJobOptions); + () => _wantToReadSyncService.Sync(CancellationToken.None), + Cron.Weekly(DayOfWeekHelper.Random()), RecurringJobOptions); } @@ -304,9 +293,9 @@ public class TaskScheduler : ITaskScheduler #region StatsTasks - public async Task ScheduleStatsTasks() + public async Task ScheduleStatsTasks(CancellationToken cancellationToken = default) { - var allowStatCollection = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).AllowStatCollection; + var allowStatCollection = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(cancellationToken)).AllowStatCollection; if (!allowStatCollection) { _logger.LogDebug("User has opted out of stat collection, not registering tasks"); @@ -317,7 +306,7 @@ public class TaskScheduler : ITaskScheduler var hour = Rnd.Next(0, 22); _logger.LogDebug("Scheduling stat collection daily at {Hour}:00", hour); - RecurringJob.AddOrUpdate(ReportStatsTaskId, () => _statsService.Send(), Cron.Daily(hour), RecurringJobOptions); + RecurringJob.AddOrUpdate(ReportStatsTaskId, () => _statsService.Send(CancellationToken.None), Cron.Daily(hour), RecurringJobOptions); } @@ -346,7 +335,7 @@ public class TaskScheduler : ITaskScheduler BackgroundJob.Schedule(() => _statsService.Send(), DateTimeOffset.Now.AddDays(1)); } - public void CovertAllCoversToEncoding() + public void ConvertAllCoversToEncoding() { var defaultParams = Array.Empty(); if (MediaConversionService.ConversionMethods.Any(method => @@ -365,8 +354,8 @@ public class TaskScheduler : ITaskScheduler public void ScheduleUpdaterTasks() { _logger.LogInformation("Scheduling Auto-Update tasks"); - RecurringJob.AddOrUpdate(CheckForUpdateId, () => CheckForUpdate(), $"0 */{Rnd.Next(4, 6)} * * *", RecurringJobOptions); - BackgroundJob.Enqueue(() => CheckForUpdate()); + RecurringJob.AddOrUpdate(CheckForUpdateId, () => CheckForUpdate(CancellationToken.None), $"0 */{Rnd.Next(4, 6)} * * *", RecurringJobOptions); + BackgroundJob.Enqueue(() => CheckForUpdate(CancellationToken.None)); } /// @@ -377,8 +366,8 @@ public class TaskScheduler : ITaskScheduler /// public void ScanFolder(string folderPath, string originalPath, TimeSpan delay) { - var normalizedFolder = Tasks.Scanner.Parser.Parser.NormalizePath(folderPath); - var normalizedOriginal = Tasks.Scanner.Parser.Parser.NormalizePath(originalPath); + var normalizedFolder = Parser.NormalizePath(folderPath); + var normalizedOriginal = Parser.NormalizePath(originalPath); if (HasAlreadyEnqueuedTask(ScannerService.Name, "ScanFolder", [normalizedFolder, normalizedOriginal]) || HasAlreadyEnqueuedTask(ScannerService.Name, "ScanFolder", [normalizedFolder, string.Empty])) @@ -397,7 +386,7 @@ public class TaskScheduler : ITaskScheduler public void ScanFolder(string folderPath, bool abortOnNoSeriesMatch = false) { - var normalizedFolder = Tasks.Scanner.Parser.Parser.NormalizePath(folderPath); + var normalizedFolder = Parser.NormalizePath(folderPath); if (HasAlreadyEnqueuedTask(ScannerService.Name, "ScanFolder", [normalizedFolder, string.Empty])) { _logger.LogTrace("Skipped scheduling ScanFolder for {Folder} as a job already queued", @@ -541,30 +530,25 @@ public class TaskScheduler : ITaskScheduler /// Not an external call. Only public so that we can call this for a Task /// // ReSharper disable once MemberCanBePrivate.Global - public async Task CheckForUpdate() + public async Task CheckForUpdate(CancellationToken cancellationToken = default) { await _defaultRetryPolicy.ExecuteAsync(async () => { - var update = await _versionUpdaterService.CheckForUpdate(); + var update = await _versionUpdaterService.CheckForUpdate(cancellationToken); if (update == null) return; - await _versionUpdaterService.PushUpdate(update); + await _versionUpdaterService.PushUpdate(update, cancellationToken); }); } - public async Task SyncThemes() - { - await _themeService.SyncThemes(); - } - /// /// Checks for any user that does not have a Library Side nav, when they should /// /// 2 users reported this issue, I cannot reproduce, this is a precaution - public async Task EnsureSideNav() + public async Task EnsureSideNav(CancellationToken ct = default) { - var users = await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.SideNavStreams); - var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync(LibraryIncludes.AppUser)).ToList(); + var users = await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.SideNavStreams, ct: ct); + var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync(LibraryIncludes.AppUser, ct: ct)).ToList(); var libraryLookup = libraries.ToDictionary(l => l.Id); // Build a lookup: userId -> set of library IDs they have access to @@ -596,7 +580,7 @@ public class TaskScheduler : ITaskScheduler var existingLibraryIds = user.SideNavStreams? .Where(s => s.LibraryId.HasValue) .Select(s => s.LibraryId!.Value) - .ToHashSet(); + .ToHashSet() ?? []; var missingLibIds = accessibleLibraryIds.Except(existingLibraryIds).ToList(); @@ -617,20 +601,20 @@ public class TaskScheduler : ITaskScheduler if (hasChanges) { - await _unitOfWork.CommitAsync(); + await _unitOfWork.CommitAsync(ct); } } /// /// Checks for soon to be expired and expired Auth keys and attempts to email the users /// - public async Task CheckExpiredOrExpiringAuthKeys() + public async Task CheckExpiredOrExpiringAuthKeys(CancellationToken ct = default) { - var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(ct); if (!settings.IsEmailSetup()) return; _logger.LogInformation("Checking for Expired or Expiring Auth Keys"); - var users = await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.AuthKeys); + var users = await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.AuthKeys, ct: ct); foreach (var user in users) { // Implies only the default keys @@ -665,7 +649,7 @@ public class TaskScheduler : ITaskScheduler foreach (var expiredKey in expiredKeys) { - await _authKeyCacheInvalidator.InvalidateAsync(expiredKey.Key); + await _authKeyService.InvalidateAsync(expiredKey.Key, ct); } } } diff --git a/API/Services/TokenService.cs b/Kavita.Services/TokenService.cs similarity index 56% rename from API/Services/TokenService.cs rename to Kavita.Services/TokenService.cs index a2a82e384..6d081cd3f 100644 --- a/API/Services/TokenService.cs +++ b/Kavita.Services/TokenService.cs @@ -6,10 +6,11 @@ using System.Security.Claims; using System.Text; using System.Threading; using System.Threading.Tasks; -using API.DTOs.Account; -using API.DTOs.Internal; -using API.Entities; -using API.Helpers; +using Kavita.API.Services; +using Kavita.Common.Helpers; +using Kavita.Models.DTOs.Account; +using Kavita.Models.DTOs.Internal; +using Kavita.Models.Entities.User; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -18,35 +19,21 @@ using static System.Security.Claims.ClaimTypes; using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames; -namespace API.Services; -#nullable enable +namespace Kavita.Services; -public interface ITokenService + +public class TokenService( + IOptions config, + UserManager userManager, + ILogger logger) + : ITokenService { - Task CreateToken(AppUser user); - Task ValidateRefreshToken(TokenRequestDto request); - Task CreateRefreshToken(AppUser user); - Task GetJwtFromUser(AppUser user); -} - - -public class TokenService : ITokenService -{ - private readonly UserManager _userManager; - private readonly ILogger _logger; - private readonly SymmetricSecurityKey _key; - private const string RefreshTokenName = "RefreshToken"; private static readonly SemaphoreSlim RefreshTokenLock = new(1, 1); - public TokenService(IOptions config, UserManager userManager, ILogger logger) - { - _userManager = userManager; - _logger = logger; + private const string RefreshTokenName = "RefreshToken"; + private readonly SymmetricSecurityKey _key = new(Encoding.UTF8.GetBytes(config.Value.TokenKey)); - _key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.Value.TokenKey)); - } - - public async Task CreateToken(AppUser user) + public async Task CreateToken(AppUser user, CancellationToken ct = default) { var claims = new List { @@ -54,7 +41,7 @@ public class TokenService : ITokenService new(NameIdentifier, user.Id.ToString()), }; - var roles = await _userManager.GetRolesAsync(user); + var roles = await userManager.GetRolesAsync(user); claims.AddRange(roles.Select(role => new Claim(Role, role))); var credentials = new SigningCredentials(_key, SecurityAlgorithms.HmacSha512Signature); @@ -71,17 +58,17 @@ public class TokenService : ITokenService return tokenHandler.WriteToken(token); } - public async Task CreateRefreshToken(AppUser user) + public async Task CreateRefreshToken(AppUser user, CancellationToken ct = default) { - await _userManager.RemoveAuthenticationTokenAsync(user, TokenOptions.DefaultProvider, RefreshTokenName); - var refreshToken = await _userManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, RefreshTokenName); - await _userManager.SetAuthenticationTokenAsync(user, TokenOptions.DefaultProvider, RefreshTokenName, refreshToken); + await userManager.RemoveAuthenticationTokenAsync(user, TokenOptions.DefaultProvider, RefreshTokenName); + var refreshToken = await userManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, RefreshTokenName); + await userManager.SetAuthenticationTokenAsync(user, TokenOptions.DefaultProvider, RefreshTokenName, refreshToken); return refreshToken; } - public async Task ValidateRefreshToken(TokenRequestDto request) + public async Task ValidateRefreshToken(TokenRequestDto request, CancellationToken ct = default) { - await RefreshTokenLock.WaitAsync(); + await RefreshTokenLock.WaitAsync(ct); try { @@ -90,46 +77,46 @@ public class TokenService : ITokenService var username = tokenContent.Claims.FirstOrDefault(q => q.Type == JwtRegisteredClaimNames.Name)?.Value; if (string.IsNullOrEmpty(username)) { - _logger.LogDebug("[RefreshToken] failed to validate due to not finding user in RefreshToken"); + logger.LogDebug("[RefreshToken] failed to validate due to not finding user in RefreshToken"); return null; } - var user = await _userManager.FindByNameAsync(username); + var user = await userManager.FindByNameAsync(username); if (user == null) { - _logger.LogDebug("[RefreshToken] failed to validate due to not finding user in DB"); + logger.LogDebug("[RefreshToken] failed to validate due to not finding user in DB"); return null; } - var validated = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, + var validated = await userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, RefreshTokenName, request.RefreshToken); if (!validated && tokenContent.ValidTo <= DateTime.UtcNow.Add(TimeSpan.FromHours(1))) { - _logger.LogDebug("[RefreshToken] failed to validate due to invalid refresh token"); + logger.LogDebug("[RefreshToken] failed to validate due to invalid refresh token"); return null; } // Remove the old refresh token first - await _userManager.RemoveAuthenticationTokenAsync(user, + await userManager.RemoveAuthenticationTokenAsync(user, TokenOptions.DefaultProvider, RefreshTokenName); return new TokenRequestDto() { - Token = await CreateToken(user), - RefreshToken = await CreateRefreshToken(user) + Token = await CreateToken(user, ct), + RefreshToken = await CreateRefreshToken(user, ct) }; } catch (SecurityTokenExpiredException ex) { // Handle expired token - _logger.LogError(ex, "Failed to validate refresh token"); + logger.LogError(ex, "Failed to validate refresh token"); return null; } catch (Exception ex) { // Handle other exceptions - _logger.LogError(ex, "Failed to validate refresh token"); + logger.LogError(ex, "Failed to validate refresh token"); return null; } finally @@ -138,9 +125,9 @@ public class TokenService : ITokenService } } - public async Task GetJwtFromUser(AppUser user) + public async Task GetJwtFromUser(AppUser user, CancellationToken ct = default) { - var userClaims = await _userManager.GetClaimsAsync(user); + var userClaims = await userManager.GetClaimsAsync(user); var jwtClaim = userClaims.FirstOrDefault(claim => claim.Type == "jwt"); return jwtClaim?.Value; } diff --git a/API/Services/Tasks/VersionUpdaterService.cs b/Kavita.Services/VersionUpdaterService.cs similarity index 97% rename from API/Services/Tasks/VersionUpdaterService.cs rename to Kavita.Services/VersionUpdaterService.cs index c8e118443..16595fb44 100644 --- a/API/Services/Tasks/VersionUpdaterService.cs +++ b/Kavita.Services/VersionUpdaterService.cs @@ -5,19 +5,21 @@ using System.IO; using System.Linq; using System.Text.Json; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; -using API.DTOs.Update; -using API.Extensions; -using API.SignalR; using Flurl.Http; +using Kavita.API.Services; +using Kavita.API.Services.SignalR; using Kavita.Common.EnvironmentInfo; +using Kavita.Common.Extensions; using Kavita.Common.Helpers; +using Kavita.Models.DTOs.SignalR; +using Kavita.Models.DTOs.Update; using MarkdownDeep; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; -namespace API.Services.Tasks; -#nullable enable +namespace Kavita.Services; internal class GithubReleaseMetadata { @@ -47,15 +49,6 @@ internal class GithubReleaseMetadata public required string Published_At { get; init; } } -public interface IVersionUpdaterService -{ - Task CheckForUpdate(); - Task PushUpdate(UpdateNotificationDto update); - Task> GetAllReleases(int count = 0); - Task GetNumberOfReleasesBehind(bool stableOnly = false); - void BustGithubCache(); -} - public partial class VersionUpdaterService : IVersionUpdaterService { @@ -103,8 +96,9 @@ public partial class VersionUpdaterService : IVersionUpdaterService /// /// Fetches the latest (stable) release from GitHub. Does not do any extra nightly release parsing. /// + /// /// Latest update - public async Task CheckForUpdate() + public async Task CheckForUpdate(CancellationToken ct = default) { // Attempt to fetch from cache var cachedRelease = await TryGetCachedLatestRelease(); @@ -150,7 +144,7 @@ public partial class VersionUpdaterService : IVersionUpdaterService var nightlyDto = new UpdateNotificationDto { - // TODO: I should pass Title to the FE so that Nightly Release can be localized + // default: I should pass Title to the FE so that Nightly Release can be localized UpdateTitle = $"Nightly Release {nightly.Version} - {prInfo.Title}", UpdateVersion = nightly.Version, CurrentVersion = dto.CurrentVersion, @@ -314,7 +308,7 @@ public partial class VersionUpdaterService : IVersionUpdaterService } } - public async Task> GetAllReleases(int count = 0) + public async Task> GetAllReleases(int count = 0, CancellationToken ct = default) { // Attempt to fetch from cache var cachedReleases = await TryGetCachedReleases(); @@ -483,10 +477,11 @@ public partial class VersionUpdaterService : IVersionUpdaterService /// then include nightly releases, otherwise only count Stable releases. /// /// Only count Stable releases + /// /// - public async Task GetNumberOfReleasesBehind(bool stableOnly = false) + public async Task GetNumberOfReleasesBehind(bool stableOnly = false, CancellationToken ct = default) { - var updates = await GetAllReleases(); + var updates = await GetAllReleases(ct: ct); // If the user is on nightly, then we need to handle releases behind differently if (!stableOnly && (updates[0].IsPrerelease || updates[0].IsOnNightlyInRelease)) @@ -502,7 +497,8 @@ public partial class VersionUpdaterService : IVersionUpdaterService /// /// Clears the Github cache /// - public void BustGithubCache() + /// + public void BustGithubCache(CancellationToken ct = default) { try { @@ -558,7 +554,7 @@ public partial class VersionUpdaterService : IVersionUpdaterService } - public async Task PushUpdate(UpdateNotificationDto? update) + public async Task PushUpdate(UpdateNotificationDto update, CancellationToken ct = default) { if (update == null) return; @@ -568,7 +564,7 @@ public partial class VersionUpdaterService : IVersionUpdaterService { _logger.LogWarning("Server is out of date. Current: {CurrentVersion}. Available: {AvailableUpdate}", BuildInfo.Version, updateVersion); await _eventHub.SendMessageAsync(MessageFactory.UpdateAvailable, MessageFactory.UpdateVersionEvent(update), - true); + true, ct); } } diff --git a/Kavita.sln b/Kavita.sln index 670808870..a279abd73 100644 --- a/Kavita.sln +++ b/Kavita.sln @@ -3,13 +3,29 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26124.0 MinimumVisualStudioVersion = 15.0.26124.0 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API", "API\API.csproj", "{1BC0273F-FEBE-4DA1-BC04-3A3167E4C86C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API.Tests", "API.Tests\API.Tests.csproj", "{6F7910F2-1B95-4570-A490-519C8935B9D1}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.Common", "Kavita.Common\Kavita.Common.csproj", "{165A86F5-9E74-4C05-9305-A6F0BA32C9EE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API.Benchmark", "API.Benchmark\API.Benchmark.csproj", "{3D781D18-2452-421F-A81A-59254FEE1FEC}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.API", "Kavita.API\Kavita.API.csproj", "{01191616-53A6-4C12-B39E-CC6B30364399}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.Server", "Kavita.Server\Kavita.Server.csproj", "{D5AED3C5-1CB2-43EC-B17B-6635FD88407B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.Services", "Kavita.Services\Kavita.Services.csproj", "{A45040F3-81F3-45C7-9EBD-BF4289E39F68}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.Models", "Kavita.Models\Kavita.Models.csproj", "{1F33A4EA-5F5D-453A-991B-BAECA4AECB65}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.Database", "Kavita.Database\Kavita.Database.csproj", "{1133F869-1D61-466C-8B33-0E3286861F25}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.Database.Tests", "Kavita.Database.Tests\Kavita.Database.Tests.csproj", "{D150983F-448E-465A-A4A4-9DC08095E22D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.Services.Tests", "Kavita.Services.Tests\Kavita.Services.Tests.csproj", "{6BA8B61A-BF07-4A7C-A491-AA5B6A6CE7A1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.Server.Tests", "Kavita.Server.Tests\Kavita.Server.Tests.csproj", "{1A3CF436-9715-4942-A584-801F3CE10A86}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.Common.Tests", "Kavita.Common.Tests\Kavita.Common.Tests.csproj", "{5C8EE151-361E-4C2C-BB1D-9828A7922876}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.Models.Tests", "Kavita.Models.Tests\Kavita.Models.Tests.csproj", "{7CAAB7E1-D5F3-4AC0-AF8E-6A0432E6A3B1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kavita.Benchmark", "Kavita.Benchmark\Kavita.Benchmark.csproj", "{CF765586-FAF5-4701-900D-C2CE8897CCC7}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -24,30 +40,6 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {1BC0273F-FEBE-4DA1-BC04-3A3167E4C86C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1BC0273F-FEBE-4DA1-BC04-3A3167E4C86C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1BC0273F-FEBE-4DA1-BC04-3A3167E4C86C}.Debug|x64.ActiveCfg = Debug|Any CPU - {1BC0273F-FEBE-4DA1-BC04-3A3167E4C86C}.Debug|x64.Build.0 = Debug|Any CPU - {1BC0273F-FEBE-4DA1-BC04-3A3167E4C86C}.Debug|x86.ActiveCfg = Debug|Any CPU - {1BC0273F-FEBE-4DA1-BC04-3A3167E4C86C}.Debug|x86.Build.0 = Debug|Any CPU - {1BC0273F-FEBE-4DA1-BC04-3A3167E4C86C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1BC0273F-FEBE-4DA1-BC04-3A3167E4C86C}.Release|Any CPU.Build.0 = Release|Any CPU - {1BC0273F-FEBE-4DA1-BC04-3A3167E4C86C}.Release|x64.ActiveCfg = Release|Any CPU - {1BC0273F-FEBE-4DA1-BC04-3A3167E4C86C}.Release|x64.Build.0 = Release|Any CPU - {1BC0273F-FEBE-4DA1-BC04-3A3167E4C86C}.Release|x86.ActiveCfg = Release|Any CPU - {1BC0273F-FEBE-4DA1-BC04-3A3167E4C86C}.Release|x86.Build.0 = Release|Any CPU - {6F7910F2-1B95-4570-A490-519C8935B9D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6F7910F2-1B95-4570-A490-519C8935B9D1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6F7910F2-1B95-4570-A490-519C8935B9D1}.Debug|x64.ActiveCfg = Debug|Any CPU - {6F7910F2-1B95-4570-A490-519C8935B9D1}.Debug|x64.Build.0 = Debug|Any CPU - {6F7910F2-1B95-4570-A490-519C8935B9D1}.Debug|x86.ActiveCfg = Debug|Any CPU - {6F7910F2-1B95-4570-A490-519C8935B9D1}.Debug|x86.Build.0 = Debug|Any CPU - {6F7910F2-1B95-4570-A490-519C8935B9D1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6F7910F2-1B95-4570-A490-519C8935B9D1}.Release|Any CPU.Build.0 = Release|Any CPU - {6F7910F2-1B95-4570-A490-519C8935B9D1}.Release|x64.ActiveCfg = Release|Any CPU - {6F7910F2-1B95-4570-A490-519C8935B9D1}.Release|x64.Build.0 = Release|Any CPU - {6F7910F2-1B95-4570-A490-519C8935B9D1}.Release|x86.ActiveCfg = Release|Any CPU - {6F7910F2-1B95-4570-A490-519C8935B9D1}.Release|x86.Build.0 = Release|Any CPU {165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Debug|Any CPU.Build.0 = Debug|Any CPU {165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -60,17 +52,137 @@ Global {165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Release|x64.Build.0 = Release|Any CPU {165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Release|x86.ActiveCfg = Release|Any CPU {165A86F5-9E74-4C05-9305-A6F0BA32C9EE}.Release|x86.Build.0 = Release|Any CPU - {3D781D18-2452-421F-A81A-59254FEE1FEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3D781D18-2452-421F-A81A-59254FEE1FEC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3D781D18-2452-421F-A81A-59254FEE1FEC}.Debug|x64.ActiveCfg = Debug|Any CPU - {3D781D18-2452-421F-A81A-59254FEE1FEC}.Debug|x64.Build.0 = Debug|Any CPU - {3D781D18-2452-421F-A81A-59254FEE1FEC}.Debug|x86.ActiveCfg = Debug|Any CPU - {3D781D18-2452-421F-A81A-59254FEE1FEC}.Debug|x86.Build.0 = Debug|Any CPU - {3D781D18-2452-421F-A81A-59254FEE1FEC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3D781D18-2452-421F-A81A-59254FEE1FEC}.Release|Any CPU.Build.0 = Release|Any CPU - {3D781D18-2452-421F-A81A-59254FEE1FEC}.Release|x64.ActiveCfg = Release|Any CPU - {3D781D18-2452-421F-A81A-59254FEE1FEC}.Release|x64.Build.0 = Release|Any CPU - {3D781D18-2452-421F-A81A-59254FEE1FEC}.Release|x86.ActiveCfg = Release|Any CPU - {3D781D18-2452-421F-A81A-59254FEE1FEC}.Release|x86.Build.0 = Release|Any CPU + {01191616-53A6-4C12-B39E-CC6B30364399}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {01191616-53A6-4C12-B39E-CC6B30364399}.Debug|Any CPU.Build.0 = Debug|Any CPU + {01191616-53A6-4C12-B39E-CC6B30364399}.Debug|x64.ActiveCfg = Debug|Any CPU + {01191616-53A6-4C12-B39E-CC6B30364399}.Debug|x64.Build.0 = Debug|Any CPU + {01191616-53A6-4C12-B39E-CC6B30364399}.Debug|x86.ActiveCfg = Debug|Any CPU + {01191616-53A6-4C12-B39E-CC6B30364399}.Debug|x86.Build.0 = Debug|Any CPU + {01191616-53A6-4C12-B39E-CC6B30364399}.Release|Any CPU.ActiveCfg = Release|Any CPU + {01191616-53A6-4C12-B39E-CC6B30364399}.Release|Any CPU.Build.0 = Release|Any CPU + {01191616-53A6-4C12-B39E-CC6B30364399}.Release|x64.ActiveCfg = Release|Any CPU + {01191616-53A6-4C12-B39E-CC6B30364399}.Release|x64.Build.0 = Release|Any CPU + {01191616-53A6-4C12-B39E-CC6B30364399}.Release|x86.ActiveCfg = Release|Any CPU + {01191616-53A6-4C12-B39E-CC6B30364399}.Release|x86.Build.0 = Release|Any CPU + {D5AED3C5-1CB2-43EC-B17B-6635FD88407B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D5AED3C5-1CB2-43EC-B17B-6635FD88407B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D5AED3C5-1CB2-43EC-B17B-6635FD88407B}.Debug|x64.ActiveCfg = Debug|Any CPU + {D5AED3C5-1CB2-43EC-B17B-6635FD88407B}.Debug|x64.Build.0 = Debug|Any CPU + {D5AED3C5-1CB2-43EC-B17B-6635FD88407B}.Debug|x86.ActiveCfg = Debug|Any CPU + {D5AED3C5-1CB2-43EC-B17B-6635FD88407B}.Debug|x86.Build.0 = Debug|Any CPU + {D5AED3C5-1CB2-43EC-B17B-6635FD88407B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D5AED3C5-1CB2-43EC-B17B-6635FD88407B}.Release|Any CPU.Build.0 = Release|Any CPU + {D5AED3C5-1CB2-43EC-B17B-6635FD88407B}.Release|x64.ActiveCfg = Release|Any CPU + {D5AED3C5-1CB2-43EC-B17B-6635FD88407B}.Release|x64.Build.0 = Release|Any CPU + {D5AED3C5-1CB2-43EC-B17B-6635FD88407B}.Release|x86.ActiveCfg = Release|Any CPU + {D5AED3C5-1CB2-43EC-B17B-6635FD88407B}.Release|x86.Build.0 = Release|Any CPU + {A45040F3-81F3-45C7-9EBD-BF4289E39F68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A45040F3-81F3-45C7-9EBD-BF4289E39F68}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A45040F3-81F3-45C7-9EBD-BF4289E39F68}.Debug|x64.ActiveCfg = Debug|Any CPU + {A45040F3-81F3-45C7-9EBD-BF4289E39F68}.Debug|x64.Build.0 = Debug|Any CPU + {A45040F3-81F3-45C7-9EBD-BF4289E39F68}.Debug|x86.ActiveCfg = Debug|Any CPU + {A45040F3-81F3-45C7-9EBD-BF4289E39F68}.Debug|x86.Build.0 = Debug|Any CPU + {A45040F3-81F3-45C7-9EBD-BF4289E39F68}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A45040F3-81F3-45C7-9EBD-BF4289E39F68}.Release|Any CPU.Build.0 = Release|Any CPU + {A45040F3-81F3-45C7-9EBD-BF4289E39F68}.Release|x64.ActiveCfg = Release|Any CPU + {A45040F3-81F3-45C7-9EBD-BF4289E39F68}.Release|x64.Build.0 = Release|Any CPU + {A45040F3-81F3-45C7-9EBD-BF4289E39F68}.Release|x86.ActiveCfg = Release|Any CPU + {A45040F3-81F3-45C7-9EBD-BF4289E39F68}.Release|x86.Build.0 = Release|Any CPU + {1F33A4EA-5F5D-453A-991B-BAECA4AECB65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F33A4EA-5F5D-453A-991B-BAECA4AECB65}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F33A4EA-5F5D-453A-991B-BAECA4AECB65}.Debug|x64.ActiveCfg = Debug|Any CPU + {1F33A4EA-5F5D-453A-991B-BAECA4AECB65}.Debug|x64.Build.0 = Debug|Any CPU + {1F33A4EA-5F5D-453A-991B-BAECA4AECB65}.Debug|x86.ActiveCfg = Debug|Any CPU + {1F33A4EA-5F5D-453A-991B-BAECA4AECB65}.Debug|x86.Build.0 = Debug|Any CPU + {1F33A4EA-5F5D-453A-991B-BAECA4AECB65}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F33A4EA-5F5D-453A-991B-BAECA4AECB65}.Release|Any CPU.Build.0 = Release|Any CPU + {1F33A4EA-5F5D-453A-991B-BAECA4AECB65}.Release|x64.ActiveCfg = Release|Any CPU + {1F33A4EA-5F5D-453A-991B-BAECA4AECB65}.Release|x64.Build.0 = Release|Any CPU + {1F33A4EA-5F5D-453A-991B-BAECA4AECB65}.Release|x86.ActiveCfg = Release|Any CPU + {1F33A4EA-5F5D-453A-991B-BAECA4AECB65}.Release|x86.Build.0 = Release|Any CPU + {1133F869-1D61-466C-8B33-0E3286861F25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1133F869-1D61-466C-8B33-0E3286861F25}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1133F869-1D61-466C-8B33-0E3286861F25}.Debug|x64.ActiveCfg = Debug|Any CPU + {1133F869-1D61-466C-8B33-0E3286861F25}.Debug|x64.Build.0 = Debug|Any CPU + {1133F869-1D61-466C-8B33-0E3286861F25}.Debug|x86.ActiveCfg = Debug|Any CPU + {1133F869-1D61-466C-8B33-0E3286861F25}.Debug|x86.Build.0 = Debug|Any CPU + {1133F869-1D61-466C-8B33-0E3286861F25}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1133F869-1D61-466C-8B33-0E3286861F25}.Release|Any CPU.Build.0 = Release|Any CPU + {1133F869-1D61-466C-8B33-0E3286861F25}.Release|x64.ActiveCfg = Release|Any CPU + {1133F869-1D61-466C-8B33-0E3286861F25}.Release|x64.Build.0 = Release|Any CPU + {1133F869-1D61-466C-8B33-0E3286861F25}.Release|x86.ActiveCfg = Release|Any CPU + {1133F869-1D61-466C-8B33-0E3286861F25}.Release|x86.Build.0 = Release|Any CPU + {D150983F-448E-465A-A4A4-9DC08095E22D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D150983F-448E-465A-A4A4-9DC08095E22D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D150983F-448E-465A-A4A4-9DC08095E22D}.Debug|x64.ActiveCfg = Debug|Any CPU + {D150983F-448E-465A-A4A4-9DC08095E22D}.Debug|x64.Build.0 = Debug|Any CPU + {D150983F-448E-465A-A4A4-9DC08095E22D}.Debug|x86.ActiveCfg = Debug|Any CPU + {D150983F-448E-465A-A4A4-9DC08095E22D}.Debug|x86.Build.0 = Debug|Any CPU + {D150983F-448E-465A-A4A4-9DC08095E22D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D150983F-448E-465A-A4A4-9DC08095E22D}.Release|Any CPU.Build.0 = Release|Any CPU + {D150983F-448E-465A-A4A4-9DC08095E22D}.Release|x64.ActiveCfg = Release|Any CPU + {D150983F-448E-465A-A4A4-9DC08095E22D}.Release|x64.Build.0 = Release|Any CPU + {D150983F-448E-465A-A4A4-9DC08095E22D}.Release|x86.ActiveCfg = Release|Any CPU + {D150983F-448E-465A-A4A4-9DC08095E22D}.Release|x86.Build.0 = Release|Any CPU + {6BA8B61A-BF07-4A7C-A491-AA5B6A6CE7A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6BA8B61A-BF07-4A7C-A491-AA5B6A6CE7A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6BA8B61A-BF07-4A7C-A491-AA5B6A6CE7A1}.Debug|x64.ActiveCfg = Debug|Any CPU + {6BA8B61A-BF07-4A7C-A491-AA5B6A6CE7A1}.Debug|x64.Build.0 = Debug|Any CPU + {6BA8B61A-BF07-4A7C-A491-AA5B6A6CE7A1}.Debug|x86.ActiveCfg = Debug|Any CPU + {6BA8B61A-BF07-4A7C-A491-AA5B6A6CE7A1}.Debug|x86.Build.0 = Debug|Any CPU + {6BA8B61A-BF07-4A7C-A491-AA5B6A6CE7A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6BA8B61A-BF07-4A7C-A491-AA5B6A6CE7A1}.Release|Any CPU.Build.0 = Release|Any CPU + {6BA8B61A-BF07-4A7C-A491-AA5B6A6CE7A1}.Release|x64.ActiveCfg = Release|Any CPU + {6BA8B61A-BF07-4A7C-A491-AA5B6A6CE7A1}.Release|x64.Build.0 = Release|Any CPU + {6BA8B61A-BF07-4A7C-A491-AA5B6A6CE7A1}.Release|x86.ActiveCfg = Release|Any CPU + {6BA8B61A-BF07-4A7C-A491-AA5B6A6CE7A1}.Release|x86.Build.0 = Release|Any CPU + {1A3CF436-9715-4942-A584-801F3CE10A86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A3CF436-9715-4942-A584-801F3CE10A86}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A3CF436-9715-4942-A584-801F3CE10A86}.Debug|x64.ActiveCfg = Debug|Any CPU + {1A3CF436-9715-4942-A584-801F3CE10A86}.Debug|x64.Build.0 = Debug|Any CPU + {1A3CF436-9715-4942-A584-801F3CE10A86}.Debug|x86.ActiveCfg = Debug|Any CPU + {1A3CF436-9715-4942-A584-801F3CE10A86}.Debug|x86.Build.0 = Debug|Any CPU + {1A3CF436-9715-4942-A584-801F3CE10A86}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A3CF436-9715-4942-A584-801F3CE10A86}.Release|Any CPU.Build.0 = Release|Any CPU + {1A3CF436-9715-4942-A584-801F3CE10A86}.Release|x64.ActiveCfg = Release|Any CPU + {1A3CF436-9715-4942-A584-801F3CE10A86}.Release|x64.Build.0 = Release|Any CPU + {1A3CF436-9715-4942-A584-801F3CE10A86}.Release|x86.ActiveCfg = Release|Any CPU + {1A3CF436-9715-4942-A584-801F3CE10A86}.Release|x86.Build.0 = Release|Any CPU + {5C8EE151-361E-4C2C-BB1D-9828A7922876}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5C8EE151-361E-4C2C-BB1D-9828A7922876}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5C8EE151-361E-4C2C-BB1D-9828A7922876}.Debug|x64.ActiveCfg = Debug|Any CPU + {5C8EE151-361E-4C2C-BB1D-9828A7922876}.Debug|x64.Build.0 = Debug|Any CPU + {5C8EE151-361E-4C2C-BB1D-9828A7922876}.Debug|x86.ActiveCfg = Debug|Any CPU + {5C8EE151-361E-4C2C-BB1D-9828A7922876}.Debug|x86.Build.0 = Debug|Any CPU + {5C8EE151-361E-4C2C-BB1D-9828A7922876}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5C8EE151-361E-4C2C-BB1D-9828A7922876}.Release|Any CPU.Build.0 = Release|Any CPU + {5C8EE151-361E-4C2C-BB1D-9828A7922876}.Release|x64.ActiveCfg = Release|Any CPU + {5C8EE151-361E-4C2C-BB1D-9828A7922876}.Release|x64.Build.0 = Release|Any CPU + {5C8EE151-361E-4C2C-BB1D-9828A7922876}.Release|x86.ActiveCfg = Release|Any CPU + {5C8EE151-361E-4C2C-BB1D-9828A7922876}.Release|x86.Build.0 = Release|Any CPU + {7CAAB7E1-D5F3-4AC0-AF8E-6A0432E6A3B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7CAAB7E1-D5F3-4AC0-AF8E-6A0432E6A3B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7CAAB7E1-D5F3-4AC0-AF8E-6A0432E6A3B1}.Debug|x64.ActiveCfg = Debug|Any CPU + {7CAAB7E1-D5F3-4AC0-AF8E-6A0432E6A3B1}.Debug|x64.Build.0 = Debug|Any CPU + {7CAAB7E1-D5F3-4AC0-AF8E-6A0432E6A3B1}.Debug|x86.ActiveCfg = Debug|Any CPU + {7CAAB7E1-D5F3-4AC0-AF8E-6A0432E6A3B1}.Debug|x86.Build.0 = Debug|Any CPU + {7CAAB7E1-D5F3-4AC0-AF8E-6A0432E6A3B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7CAAB7E1-D5F3-4AC0-AF8E-6A0432E6A3B1}.Release|Any CPU.Build.0 = Release|Any CPU + {7CAAB7E1-D5F3-4AC0-AF8E-6A0432E6A3B1}.Release|x64.ActiveCfg = Release|Any CPU + {7CAAB7E1-D5F3-4AC0-AF8E-6A0432E6A3B1}.Release|x64.Build.0 = Release|Any CPU + {7CAAB7E1-D5F3-4AC0-AF8E-6A0432E6A3B1}.Release|x86.ActiveCfg = Release|Any CPU + {7CAAB7E1-D5F3-4AC0-AF8E-6A0432E6A3B1}.Release|x86.Build.0 = Release|Any CPU + {CF765586-FAF5-4701-900D-C2CE8897CCC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF765586-FAF5-4701-900D-C2CE8897CCC7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF765586-FAF5-4701-900D-C2CE8897CCC7}.Debug|x64.ActiveCfg = Debug|Any CPU + {CF765586-FAF5-4701-900D-C2CE8897CCC7}.Debug|x64.Build.0 = Debug|Any CPU + {CF765586-FAF5-4701-900D-C2CE8897CCC7}.Debug|x86.ActiveCfg = Debug|Any CPU + {CF765586-FAF5-4701-900D-C2CE8897CCC7}.Debug|x86.Build.0 = Debug|Any CPU + {CF765586-FAF5-4701-900D-C2CE8897CCC7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF765586-FAF5-4701-900D-C2CE8897CCC7}.Release|Any CPU.Build.0 = Release|Any CPU + {CF765586-FAF5-4701-900D-C2CE8897CCC7}.Release|x64.ActiveCfg = Release|Any CPU + {CF765586-FAF5-4701-900D-C2CE8897CCC7}.Release|x64.Build.0 = Release|Any CPU + {CF765586-FAF5-4701-900D-C2CE8897CCC7}.Release|x86.ActiveCfg = Release|Any CPU + {CF765586-FAF5-4701-900D-C2CE8897CCC7}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index 3edec9f7a..3ffe1c14a 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -9,25 +9,25 @@ "version": "0.7.12.1", "dependencies": { "@angular-slider/ngx-slider": "^21.0.0", - "@angular/animations": "^21.0.6", - "@angular/cdk": "^21.0.3", - "@angular/common": "^21.0.6", - "@angular/compiler": "^21.0.6", - "@angular/core": "^21.0.6", - "@angular/forms": "^21.0.6", - "@angular/localize": "^21.0.6", - "@angular/platform-browser": "^21.0.6", - "@angular/platform-browser-dynamic": "^21.0.6", - "@angular/router": "^21.0.6", - "@fortawesome/fontawesome-free": "^7.1.0", - "@iharbeck/ngx-virtual-scroller": "^19.0.1", + "@angular/animations": "^21.2.1", + "@angular/cdk": "^21.2.1", + "@angular/common": "^21.2.1", + "@angular/compiler": "^21.2.1", + "@angular/core": "^21.2.1", + "@angular/forms": "^21.2.1", + "@angular/localize": "^21.2.1", + "@angular/platform-browser": "^21.2.1", + "@angular/platform-browser-dynamic": "^21.2.1", + "@angular/router": "^21.2.1", + "@fortawesome/fontawesome-free": "^7.2.0", + "@iharbeck/ngx-virtual-scroller": "^20.0.0", "@iplab/ngx-color-picker": "^21.0.0", "@iplab/ngx-file-upload": "^21.0.0", - "@jsverse/transloco": "^8.2.0", - "@jsverse/transloco-locale": "^8.2.0", - "@jsverse/transloco-persist-lang": "^8.2.0", - "@jsverse/transloco-persist-translations": "^8.2.0", - "@jsverse/transloco-preload-langs": "^8.2.0", + "@jsverse/transloco": "^8.2.1", + "@jsverse/transloco-locale": "^8.2.1", + "@jsverse/transloco-persist-lang": "^8.2.1", + "@jsverse/transloco-persist-translations": "^8.2.1", + "@jsverse/transloco-preload-langs": "^8.2.1", "@microsoft/signalr": "^10.0.0", "@ng-bootstrap/ng-bootstrap": "^20.0.0", "@popperjs/core": "^2.11.7", @@ -49,89 +49,89 @@ "ngx-stars": "^1.6.5", "ngx-toastr": "^19.1.0", "nosleep.js": "^0.12.0", - "quill": "^2.0.3", + "quill": "^2.0.2", "rxjs": "^7.8.2", "screenfull": "^6.0.2", - "swiper": "^12.0.3", + "swiper": "^12.1.2", "tslib": "^2.8.1", - "zone.js": "^0.16.0" + "zone.js": "^0.16.1" }, "devDependencies": { - "@angular-eslint/builder": "^21.1.0", - "@angular-eslint/eslint-plugin": "^21.1.0", - "@angular-eslint/eslint-plugin-template": "^21.1.0", - "@angular-eslint/schematics": "^21.1.0", - "@angular-eslint/template-parser": "^21.1.0", - "@angular/build": "^21.0.3", - "@angular/cli": "^21.0.3", - "@angular/compiler-cli": "^21.0.6", + "@angular-eslint/builder": "^21.3.0", + "@angular-eslint/eslint-plugin": "^21.3.0", + "@angular-eslint/eslint-plugin-template": "^21.3.0", + "@angular-eslint/schematics": "^21.3.0", + "@angular-eslint/template-parser": "^21.3.0", + "@angular/build": "^21.2.1", + "@angular/cli": "^21.2.1", + "@angular/compiler-cli": "^21.2.1", "@types/d3": "^7.4.3", "@types/file-saver": "^2.0.7", "@types/luxon": "^3.7.1", "@types/marked": "^5.0.2", - "@types/node": "^25.0.2", - "@typescript-eslint/eslint-plugin": "^8.50.0", - "@typescript-eslint/parser": "^8.50.0", - "eslint": "^9.39.2", + "@types/node": "^25.3.3", + "@typescript-eslint/eslint-plugin": "^8.56.1", + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^10.0.2", "jsonminify": "^0.4.2", "karma-coverage": "~2.2.0", "ts-node": "~10.9.1", "typescript": "^5.9.3", - "webpack-bundle-analyzer": "^5.1.0" + "webpack-bundle-analyzer": "^5.2.0" } }, "node_modules/@algolia/abtesting": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.6.1.tgz", - "integrity": "sha512-wV/gNRkzb7sI9vs1OneG129hwe3Q5zPj7zigz3Ps7M5Lpo2hSorrOnXNodHEOV+yXE/ks4Pd+G3CDFIjFTWhMQ==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.14.1.tgz", + "integrity": "sha512-Dkj0BgPiLAaim9sbQ97UKDFHJE/880wgStAM18U++NaJ/2Cws34J5731ovJifr6E3Pv4T2CqvMXf8qLCC417Ew==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.40.1", - "@algolia/requester-browser-xhr": "5.40.1", - "@algolia/requester-fetch": "5.40.1", - "@algolia/requester-node-http": "5.40.1" + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-abtesting": { - "version": "5.40.1", - "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.40.1.tgz", - "integrity": "sha512-cxKNATPY5t+Mv8XAVTI57altkaPH+DZi4uMrnexPxPHODMljhGYY+GDZyHwv9a+8CbZHcY372OkxXrDMZA4Lnw==", + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.48.1.tgz", + "integrity": "sha512-LV5qCJdj+/m9I+Aj91o+glYszrzd7CX6NgKaYdTOj4+tUYfbS62pwYgUfZprYNayhkQpVFcrW8x8ZlIHpS23Vw==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.40.1", - "@algolia/requester-browser-xhr": "5.40.1", - "@algolia/requester-fetch": "5.40.1", - "@algolia/requester-node-http": "5.40.1" + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-analytics": { - "version": "5.40.1", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.40.1.tgz", - "integrity": "sha512-XP008aMffJCRGAY8/70t+hyEyvqqV7YKm502VPu0+Ji30oefrTn2al7LXkITz7CK6I4eYXWRhN6NaIUi65F1OA==", + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.48.1.tgz", + "integrity": "sha512-/AVoMqHhPm14CcHq7mwB+bUJbfCv+jrxlNvRjXAuO+TQa+V37N8k1b0ijaRBPdmSjULMd8KtJbQyUyabXOu6Kg==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.40.1", - "@algolia/requester-browser-xhr": "5.40.1", - "@algolia/requester-fetch": "5.40.1", - "@algolia/requester-node-http": "5.40.1" + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-common": { - "version": "5.40.1", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.40.1.tgz", - "integrity": "sha512-gWfQuQUBtzUboJv/apVGZMoxSaB0M4Imwl1c9Ap+HpCW7V0KhjBddqF2QQt5tJZCOFsfNIgBbZDGsEPaeKUosw==", + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.48.1.tgz", + "integrity": "sha512-VXO+qu2Ep6ota28ktvBm3sG53wUHS2n7bgLWmce5jTskdlCD0/JrV4tnBm1l7qpla1CeoQb8D7ShFhad+UoSOw==", "dev": true, "license": "MIT", "engines": { @@ -139,151 +139,151 @@ } }, "node_modules/@algolia/client-insights": { - "version": "5.40.1", - "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.40.1.tgz", - "integrity": "sha512-RTLjST/t+lsLMouQ4zeLJq2Ss+UNkLGyNVu+yWHanx6kQ3LT5jv8UvPwyht9s7R6jCPnlSI77WnL80J32ZuyJg==", + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.48.1.tgz", + "integrity": "sha512-zl+Qyb0nLg+Y5YvKp1Ij+u9OaPaKg2/EPzTwKNiVyOHnQJlFxmXyUZL1EInczAZsEY8hVpPCLtNfhMhfxluXKQ==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.40.1", - "@algolia/requester-browser-xhr": "5.40.1", - "@algolia/requester-fetch": "5.40.1", - "@algolia/requester-node-http": "5.40.1" + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-personalization": { - "version": "5.40.1", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.40.1.tgz", - "integrity": "sha512-2FEK6bUomBzEYkTKzD0iRs7Ljtjb45rKK/VSkyHqeJnG+77qx557IeSO0qVFE3SfzapNcoytTofnZum0BQ6r3Q==", + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.48.1.tgz", + "integrity": "sha512-r89Qf9Oo9mKWQXumRu/1LtvVJAmEDpn8mHZMc485pRfQUMAwSSrsnaw1tQ3sszqzEgAr1c7rw6fjBI+zrAXTOw==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.40.1", - "@algolia/requester-browser-xhr": "5.40.1", - "@algolia/requester-fetch": "5.40.1", - "@algolia/requester-node-http": "5.40.1" + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-query-suggestions": { - "version": "5.40.1", - "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.40.1.tgz", - "integrity": "sha512-Nju4NtxAvXjrV2hHZNLKVJLXjOlW6jAXHef/CwNzk1b2qIrCWDO589ELi5ZHH1uiWYoYyBXDQTtHmhaOVVoyXg==", + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.48.1.tgz", + "integrity": "sha512-TPKNPKfghKG/bMSc7mQYD9HxHRUkBZA4q1PEmHgICaSeHQscGqL4wBrKkhfPlDV1uYBKW02pbFMUhsOt7p4ZpA==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.40.1", - "@algolia/requester-browser-xhr": "5.40.1", - "@algolia/requester-fetch": "5.40.1", - "@algolia/requester-node-http": "5.40.1" + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-search": { - "version": "5.40.1", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.40.1.tgz", - "integrity": "sha512-Mw6pAUF121MfngQtcUb5quZVqMC68pSYYjCRZkSITC085S3zdk+h/g7i6FxnVdbSU6OztxikSDMh1r7Z+4iPlA==", + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.48.1.tgz", + "integrity": "sha512-4Fu7dnzQyQmMFknYwTiN/HxPbH4DyxvQ1m+IxpPp5oslOgz8m6PG5qhiGbqJzH4HiT1I58ecDiCAC716UyVA8Q==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.40.1", - "@algolia/requester-browser-xhr": "5.40.1", - "@algolia/requester-fetch": "5.40.1", - "@algolia/requester-node-http": "5.40.1" + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/ingestion": { - "version": "1.40.1", - "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.40.1.tgz", - "integrity": "sha512-z+BPlhs45VURKJIxsR99NNBWpUEEqIgwt10v/fATlNxc4UlXvALdOsWzaFfe89/lbP5Bu4+mbO59nqBC87ZM/g==", + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.48.1.tgz", + "integrity": "sha512-/RFq3TqtXDUUawwic/A9xylA2P3LDMO8dNhphHAUOU51b1ZLHrmZ6YYJm3df1APz7xLY1aht6okCQf+/vmrV9w==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.40.1", - "@algolia/requester-browser-xhr": "5.40.1", - "@algolia/requester-fetch": "5.40.1", - "@algolia/requester-node-http": "5.40.1" + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/monitoring": { - "version": "1.40.1", - "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.40.1.tgz", - "integrity": "sha512-VJMUMbO0wD8Rd2VVV/nlFtLJsOAQvjnVNGkMkspFiFhpBA7s/xJOb+fJvvqwKFUjbKTUA7DjiSi1ljSMYBasXg==", + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.48.1.tgz", + "integrity": "sha512-Of0jTeAZRyRhC7XzDSjJef0aBkgRcvRAaw0ooYRlOw57APii7lZdq+layuNdeL72BRq1snaJhoMMwkmLIpJScw==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.40.1", - "@algolia/requester-browser-xhr": "5.40.1", - "@algolia/requester-fetch": "5.40.1", - "@algolia/requester-node-http": "5.40.1" + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/recommend": { - "version": "5.40.1", - "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.40.1.tgz", - "integrity": "sha512-ehvJLadKVwTp9Scg9NfzVSlBKH34KoWOQNTaN8i1Ac64AnO6iH2apJVSP6GOxssaghZ/s8mFQsDH3QIZoluFHA==", + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.48.1.tgz", + "integrity": "sha512-bE7JcpFXzxF5zHwj/vkl2eiCBvyR1zQ7aoUdO+GDXxGp0DGw7nI0p8Xj6u8VmRQ+RDuPcICFQcCwRIJT5tDJFw==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.40.1", - "@algolia/requester-browser-xhr": "5.40.1", - "@algolia/requester-fetch": "5.40.1", - "@algolia/requester-node-http": "5.40.1" + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-browser-xhr": { - "version": "5.40.1", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.40.1.tgz", - "integrity": "sha512-PbidVsPurUSQIr6X9/7s34mgOMdJnn0i6p+N6Ab+lsNhY5eiu+S33kZEpZwkITYBCIbhzDLOvb7xZD3gDi+USA==", + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.48.1.tgz", + "integrity": "sha512-MK3wZ2koLDnvH/AmqIF1EKbJlhRS5j74OZGkLpxI4rYvNi9Jn/C7vb5DytBnQ4KUWts7QsmbdwHkxY5txQHXVw==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.40.1" + "@algolia/client-common": "5.48.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-fetch": { - "version": "5.40.1", - "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.40.1.tgz", - "integrity": "sha512-ThZ5j6uOZCF11fMw9IBkhigjOYdXGXQpj6h4k+T9UkZrF2RlKcPynFzDeRgaLdpYk8Yn3/MnFbwUmib7yxj5Lw==", + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.48.1.tgz", + "integrity": "sha512-2oDT43Y5HWRSIQMPQI4tA/W+TN/N2tjggZCUsqQV440kxzzoPGsvv9QP1GhQ4CoDa+yn6ygUsGp6Dr+a9sPPSg==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.40.1" + "@algolia/client-common": "5.48.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-node-http": { - "version": "5.40.1", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.40.1.tgz", - "integrity": "sha512-H1gYPojO6krWHnUXu/T44DrEun/Wl95PJzMXRcM/szstNQczSbwq6wIFJPI9nyE95tarZfUNU3rgorT+wZ6iCQ==", + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.48.1.tgz", + "integrity": "sha512-xcaCqbhupVWhuBP1nwbk1XNvwrGljozutEiLx06mvqDf3o8cHyEgQSHS4fKJM+UAggaWVnnFW+Nne5aQ8SUJXg==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.40.1" + "@algolia/client-common": "5.48.1" }, "engines": { "node": ">= 14.0.0" @@ -304,21 +304,103 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.2100.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2100.5.tgz", - "integrity": "sha512-KKmZMXzHCX0cWHY7xo9yy1J0fV7S/suhPO00YTcHBgLivkLsnbI177CrmWiMdLxSJD3NqTVkBEMPFQ2I2ooDFw==", + "version": "0.2102.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.1.tgz", + "integrity": "sha512-x2Qqz6oLYvEh9UBUG0AP1A4zROO/VP+k+zM9+4c2uZw1uqoBQFmutqgzncjVU7cR9R0RApgx9JRZHDFtQru68w==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.0.5", + "@angular-devkit/core": "21.2.1", "rxjs": "7.8.2" }, + "bin": { + "architect": "bin/cli.js" + }, "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, + "node_modules/@angular-devkit/architect/node_modules/@angular-devkit/core": { + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.1.tgz", + "integrity": "sha512-TpXGjERqVPN8EPt7LdmWAwh0oNQ/6uWFutzGZiXhJy81n1zb1O1XrqhRAmvP1cAo5O+na6IV2JkkCmxL6F8GUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.3", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/architect/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular-devkit/architect/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@angular-devkit/architect/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@angular-devkit/core": { "version": "21.0.5", "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.0.5.tgz", @@ -348,16 +430,16 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "21.0.5", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.0.5.tgz", - "integrity": "sha512-U6Z/OEce3R9CJl8/xuVrNVp0uhv3Ac4wRjpG18kE0dh5R87ablhqr/wkP3rZbWpdGwuGSJ+cR7LE5IbwSswejA==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.1.tgz", + "integrity": "sha512-CWoamHaasAHMjHcYqxbj0tMnoXxdGotcAz2SpiuWtH28Lnf5xfbTaJn/lwdMP8Wdh4tgA+uYh2l45A5auCwmkw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.0.5", + "@angular-devkit/core": "21.2.1", "jsonc-parser": "3.3.1", - "magic-string": "0.30.19", - "ora": "9.0.0", + "magic-string": "0.30.21", + "ora": "9.3.0", "rxjs": "7.8.2" }, "engines": { @@ -366,10 +448,89 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.1.tgz", + "integrity": "sha512-TpXGjERqVPN8EPt7LdmWAwh0oNQ/6uWFutzGZiXhJy81n1zb1O1XrqhRAmvP1cAo5O+na6IV2JkkCmxL6F8GUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.3", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@angular-eslint/builder": { - "version": "21.1.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-21.1.0.tgz", - "integrity": "sha512-pcUlDkGqeZ+oQC0oEjnkDDlB96gbgHQhnBUKdhYAiAOSuiBod4+npP0xQOq5chYtRNPBprhDqgrJrp5DBeDMOA==", + "version": "21.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-21.3.0.tgz", + "integrity": "sha512-26QUUouei52biUFAlJSrWNAU9tuF2miKwd8uHdxWwCF31xz+OxC5+NfudWvt1AFaYow7gWueX1QX3rNNtSPDrg==", "dev": true, "license": "MIT", "dependencies": { @@ -378,67 +539,67 @@ }, "peerDependencies": { "@angular/cli": ">= 21.0.0 < 22.0.0", - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": "*" } }, "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "21.1.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-21.1.0.tgz", - "integrity": "sha512-t52J6FszgEHaJ+IjuzU9qaWfVxsjlVNkAP+B5z2t4NDgbbDDsmI+QJh0OtP1qdlqzjh2pbocEml30KhYmNZm/Q==", + "version": "21.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-21.3.0.tgz", + "integrity": "sha512-l521I24J9gJxyMbRkrM24Tc7W8J8BP+TDAmVs2nT8+lXbS3kg8QpWBRtd+hNUgq6o+vt+lKBkytnEfu8OiqeRg==", "dev": true, "license": "MIT" }, "node_modules/@angular-eslint/eslint-plugin": { - "version": "21.1.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-21.1.0.tgz", - "integrity": "sha512-oNp+4UzN2M3KwGwEw03NUdXz93vqJd9sMzTbGXWF9+KVfA2LjckGDTrI6g6asGcJMdyTo07rDcnw0m0MkLB5VA==", + "version": "21.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-21.3.0.tgz", + "integrity": "sha512-Whf/AUUBekOlfSJRS78m76YGrBQAZ3waXE7oOdlW5xEQvn8jBDN9EGuNnjg/syZzvzjK4ZpYC4g1XYXrc+fQIg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.1.0", - "@angular-eslint/utils": "21.1.0", + "@angular-eslint/bundled-angular-compiler": "21.3.0", + "@angular-eslint/utils": "21.3.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": "*" } }, "node_modules/@angular-eslint/eslint-plugin-template": { - "version": "21.1.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-21.1.0.tgz", - "integrity": "sha512-FlbRfOCn8IUHvP1ebcCSQFVNh+4X/HqZqL7SW5oj9WIYPiOX9ijS03ndNbfX/pBPSIi8GHLKMjLt8zIy1l5Lww==", + "version": "21.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-21.3.0.tgz", + "integrity": "sha512-lVixd/KypPWgA/5/pUOhJV9MTcaHjYZEqyOi+IiLk+h+maGxn6/s6Ot+20n+XGS85zAgOY+qUw6EEQ11hoojIQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.1.0", - "@angular-eslint/utils": "21.1.0", + "@angular-eslint/bundled-angular-compiler": "21.3.0", + "@angular-eslint/utils": "21.3.0", "aria-query": "5.3.2", "axobject-query": "4.1.0" }, "peerDependencies": { - "@angular-eslint/template-parser": "21.1.0", + "@angular-eslint/template-parser": "21.3.0", "@typescript-eslint/types": "^7.11.0 || ^8.0.0", "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": "*" } }, "node_modules/@angular-eslint/schematics": { - "version": "21.1.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-21.1.0.tgz", - "integrity": "sha512-Hal1mYwx4MTjCcNHqfIlua31xrk2tZJoyTiXiGQ21cAeK4sFuY+9V7/8cxbwJMGftX0G4J7uhx8woOdIFuqiZw==", + "version": "21.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-21.3.0.tgz", + "integrity": "sha512-8deU/zVY9f8k8kAQQ9PL130ox2VlrZw3fMxgsPNAY5tjQ0xk0J2YVSszYHhcqdMGG1J01IsxIjvQaJ4pFfEmMw==", "dev": true, "license": "MIT", "dependencies": { "@angular-devkit/core": ">= 21.0.0 < 22.0.0", "@angular-devkit/schematics": ">= 21.0.0 < 22.0.0", - "@angular-eslint/eslint-plugin": "21.1.0", - "@angular-eslint/eslint-plugin-template": "21.1.0", + "@angular-eslint/eslint-plugin": "21.3.0", + "@angular-eslint/eslint-plugin-template": "21.3.0", "ignore": "7.0.5", - "semver": "7.7.3", + "semver": "7.7.4", "strip-json-comments": "3.1.1" }, "peerDependencies": { @@ -446,32 +607,32 @@ } }, "node_modules/@angular-eslint/template-parser": { - "version": "21.1.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-21.1.0.tgz", - "integrity": "sha512-PYVgNbjNtuD5/QOuS6cHR8A7bRqsVqxtUUXGqdv76FYMAajQcAvyfR0QxOkqf3NmYxgNgO3hlUHWq0ILjVbcow==", + "version": "21.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-21.3.0.tgz", + "integrity": "sha512-ysyou1zAY6M6rSZNdIcYKGd4nk6TCapamyFNB3ivmTlVZ0O35TS9o/rJ0aUttuHgDp+Ysgs3ql+LA746PXgCyQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.1.0", - "eslint-scope": "^9.0.0" + "@angular-eslint/bundled-angular-compiler": "21.3.0", + "eslint-scope": "^9.1.1" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": "*" } }, "node_modules/@angular-eslint/utils": { - "version": "21.1.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-21.1.0.tgz", - "integrity": "sha512-rWINgxGREu+NFUPCpAVsBGG8B4hfXxyswM0N5GbjykvsfB5W6PUix2Gsoh++iEsZPT+c9lvgXL5GbpwfanjOow==", + "version": "21.3.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-21.3.0.tgz", + "integrity": "sha512-oNigH6w3l+owTMboj/uFG0tHOy43uH8BpQRtBOQL1/s2+5in/BJ2Fjobv3SyizxTgeJ1FhRefbkT8GmVjK7jAA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.1.0" + "@angular-eslint/bundled-angular-compiler": "21.3.0" }, "peerDependencies": { "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": "*" } }, @@ -492,9 +653,9 @@ } }, "node_modules/@angular/animations": { - "version": "21.0.7", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.0.7.tgz", - "integrity": "sha512-TfGE+emi67LAIUYmyiHfnL8BVqk26ZZVNEz7hDfbFztbZ5qhtHeKoG+97bAKtJDTTkxgs1JvB8escZExe1JkdA==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.2.1.tgz", + "integrity": "sha512-zT/S29pUTbziCLvZ2itBdNWd5i8tsXexofH7KA4n2yvYmK1EhNpE7TlHRjghmsHgtDt4VnGiMW4zXEyrl05Dwg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -503,43 +664,43 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/core": "21.0.7" + "@angular/core": "21.2.1" } }, "node_modules/@angular/build": { - "version": "21.0.5", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.0.5.tgz", - "integrity": "sha512-4Ejb5pA118GGyZOAGjSmZMCx5HbovRSjiqLuCmpjf9hUgs50GPNJbigWW1ewz5+KmFrc8ouEoirpgTQyaKKZ3Q==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.1.tgz", + "integrity": "sha512-cUpLNHJp9taII/FOcJHHfQYlMcZSRaf6eIxgSNS6Xfx1CeGoJNDN+J8+GFk+H1CPJt1EvbfyZ+dE5DbsgTD/QQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.2100.5", - "@babel/core": "7.28.4", + "@angular-devkit/architect": "0.2102.1", + "@babel/core": "7.29.0", "@babel/helper-annotate-as-pure": "7.27.3", "@babel/helper-split-export-declaration": "7.24.7", - "@inquirer/confirm": "5.1.19", - "@vitejs/plugin-basic-ssl": "2.1.0", - "beasties": "0.3.5", + "@inquirer/confirm": "5.1.21", + "@vitejs/plugin-basic-ssl": "2.1.4", + "beasties": "0.4.1", "browserslist": "^4.26.0", - "esbuild": "0.26.0", + "esbuild": "0.27.3", "https-proxy-agent": "7.0.6", "istanbul-lib-instrument": "6.0.3", "jsonc-parser": "3.3.1", "listr2": "9.0.5", - "magic-string": "0.30.19", + "magic-string": "0.30.21", "mrmime": "2.0.1", "parse5-html-rewriting-stream": "8.0.0", "picomatch": "4.0.3", - "piscina": "5.1.3", - "rolldown": "1.0.0-beta.47", - "sass": "1.93.2", - "semver": "7.7.3", + "piscina": "5.1.4", + "rolldown": "1.0.0-rc.4", + "sass": "1.97.3", + "semver": "7.7.4", "source-map-support": "0.5.21", "tinyglobby": "0.2.15", - "undici": "7.16.0", - "vite": "7.2.2", - "watchpack": "2.4.4" + "undici": "7.22.0", + "vite": "7.3.1", + "watchpack": "2.5.1" }, "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0", @@ -547,7 +708,7 @@ "yarn": ">= 1.13.0" }, "optionalDependencies": { - "lmdb": "3.4.3" + "lmdb": "3.5.1" }, "peerDependencies": { "@angular/compiler": "^21.0.0", @@ -557,7 +718,7 @@ "@angular/platform-browser": "^21.0.0", "@angular/platform-server": "^21.0.0", "@angular/service-worker": "^21.0.0", - "@angular/ssr": "^21.0.5", + "@angular/ssr": "^21.2.1", "karma": "^6.4.0", "less": "^4.2.0", "ng-packagr": "^21.0.0", @@ -607,9 +768,9 @@ } }, "node_modules/@angular/cdk": { - "version": "21.0.5", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.0.5.tgz", - "integrity": "sha512-yO/IRYEZ5wJkpwg3GT3b6RST4pqNFTAhuyPdEdLcE81cs283K3aKOsCYh2xUR3bR4WxBh2kBPSJ31AFZyJXbSA==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.2.1.tgz", + "integrity": "sha512-JUFV8qLnO7CU5v4W0HzXSQrFkkJ4RH/qqdwrf9lup7YEnsLxB7cTGhsVisc9pWKAJsoNZ4pXCVOkqKc1mFL7dw==", "license": "MIT", "dependencies": { "parse5": "^8.0.0", @@ -618,35 +779,35 @@ "peerDependencies": { "@angular/common": "^21.0.0 || ^22.0.0", "@angular/core": "^21.0.0 || ^22.0.0", + "@angular/platform-browser": "^21.0.0 || ^22.0.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/cli": { - "version": "21.0.5", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.0.5.tgz", - "integrity": "sha512-UYFQqn9Ow1wFVSwdB/xfjmZo4Yb7CUNxilbeYDFIybesfxXSdjMJBbXLtV0+icIhjmqfSUm2gTls6WIrG8qv9A==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.1.tgz", + "integrity": "sha512-5SRfMTgwFj1zXOpfeZWHsxZBni0J4Xz7/CbewG47D6DmbstOrSdgt6eNzJ62R650t0G9dpri2YvToZgImtbjOQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.2100.5", - "@angular-devkit/core": "21.0.5", - "@angular-devkit/schematics": "21.0.5", - "@inquirer/prompts": "7.9.0", + "@angular-devkit/architect": "0.2102.1", + "@angular-devkit/core": "21.2.1", + "@angular-devkit/schematics": "21.2.1", + "@inquirer/prompts": "7.10.1", "@listr2/prompt-adapter-inquirer": "3.0.5", - "@modelcontextprotocol/sdk": "1.25.2", - "@schematics/angular": "21.0.5", + "@modelcontextprotocol/sdk": "1.26.0", + "@schematics/angular": "21.2.1", "@yarnpkg/lockfile": "1.1.0", - "algoliasearch": "5.40.1", - "ini": "5.0.0", + "algoliasearch": "5.48.1", + "ini": "6.0.0", "jsonc-parser": "3.3.1", "listr2": "9.0.5", - "npm-package-arg": "13.0.1", - "pacote": "21.0.3", + "npm-package-arg": "13.0.2", + "pacote": "21.3.1", "parse5-html-rewriting-stream": "8.0.0", - "resolve": "1.22.11", - "semver": "7.7.3", + "semver": "7.7.4", "yargs": "18.0.0", - "zod": "4.1.13" + "zod": "4.3.6" }, "bin": { "ng": "bin/ng.js" @@ -657,10 +818,89 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular/cli/node_modules/@angular-devkit/core": { + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.1.tgz", + "integrity": "sha512-TpXGjERqVPN8EPt7LdmWAwh0oNQ/6uWFutzGZiXhJy81n1zb1O1XrqhRAmvP1cAo5O+na6IV2JkkCmxL6F8GUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.3", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular/cli/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular/cli/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@angular/cli/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@angular/common": { - "version": "21.0.7", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.0.7.tgz", - "integrity": "sha512-KNstFFCv6//x33F+YBPEIztDSNBVyLH99C8yFPmb7vawxGbR9liKSHC1WnEk+GR5KgV3I5lFOJyWL7Elfm0K5A==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.1.tgz", + "integrity": "sha512-xhv2i1Q9s1kpGbGsfj+o36+XUC/TQLcZyRuRxn3GwaN7Rv34FabC88ycpvoE+sW/txj4JRx9yPA0dRSZjwZ+Gg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -669,14 +909,14 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/core": "21.0.7", + "@angular/core": "21.2.1", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "21.0.7", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.0.7.tgz", - "integrity": "sha512-Qsjx0OrOquyx10fMynkHilRRoZy9qJcstHdML7NGKg887xqHW4YvgNKREIXmKYjnV6sUBBUxJUD1L5ouarb/YA==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.1.tgz", + "integrity": "sha512-FxWaSaii1vfHIFA+JksqQ8NGB2frfqCrs7Ju50a44kbwR4fmanfn/VsiS/CbwBp9vcyT/Br9X/jAG4RuK/U2nw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -686,14 +926,14 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "21.0.7", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.0.7.tgz", - "integrity": "sha512-M4ePAA7AwjTsbUq6Qpremgo7qIP9GIgWqV5FoJPUEthtFGPNEiKGYjpOtXJ/OLB1J2Tn0ygrqe0PAYE0YxeEUA==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.1.tgz", + "integrity": "sha512-qYCWLGtEju4cDtYLi4ZzbwKoF0lcGs+Lc31kuESvAzYvWNgk2EUOtwWo8kbgpAzAwSYodtxW6Q90iWEwfU6elw==", "license": "MIT", "dependencies": { - "@babel/core": "7.28.4", + "@babel/core": "7.29.0", "@jridgewell/sourcemap-codec": "^1.4.14", - "chokidar": "^4.0.0", + "chokidar": "^5.0.0", "convert-source-map": "^1.5.1", "reflect-metadata": "^0.2.0", "semver": "^7.0.0", @@ -708,8 +948,8 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/compiler": "21.0.7", - "typescript": ">=5.9 <6.0" + "@angular/compiler": "21.2.1", + "typescript": ">=5.9 <6.1" }, "peerDependenciesMeta": { "typescript": { @@ -717,10 +957,38 @@ } } }, + "node_modules/@angular/compiler-cli/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@angular/compiler-cli/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@angular/core": { - "version": "21.0.7", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.0.7.tgz", - "integrity": "sha512-MvgRRse2PaEleQFp+35rj7ew5gBmBh3wp5yNDYPTiPaVp1I3fJ08VYSpldodaXmdkdWRB+OU4WJhnFkagyRx7A==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.1.tgz", + "integrity": "sha512-pFTbg03s2ZI5cHNT+eWsGjwIIKiYkeAnodFbCAHjwFi9KCEYlTykFLjr9lcpGrBddfmAH7GE08Q73vgmsdcNHw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -729,7 +997,7 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/compiler": "21.0.7", + "@angular/compiler": "21.2.1", "rxjs": "^6.5.3 || ^7.4.0", "zone.js": "~0.15.0 || ~0.16.0" }, @@ -743,9 +1011,9 @@ } }, "node_modules/@angular/forms": { - "version": "21.0.7", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.0.7.tgz", - "integrity": "sha512-HUfUaO6+cxam9wug3Upc83ueBIDSgJwxzYIuPCP4AjL5DhT6Fbqv/Zq+nLbLF7rklbKdqzYsMjse97pxmxJGLQ==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.1.tgz", + "integrity": "sha512-6aqOPk9xoa0dfeUDeEbhaiPhmt6MQrdn59qbGAomn9RMXA925TrHbJhSIkp9tXc2Fr4aJRi8zkD/cdXEc1IYeA==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -755,19 +1023,19 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "21.0.7", - "@angular/core": "21.0.7", - "@angular/platform-browser": "21.0.7", + "@angular/common": "21.2.1", + "@angular/core": "21.2.1", + "@angular/platform-browser": "21.2.1", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/localize": { - "version": "21.0.7", - "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-21.0.7.tgz", - "integrity": "sha512-H1BdOOe0prtQa/EjWyzyZ9Ls4dPHcPNK/oN4fAYkpaZzyyqhvmPU64TYHa/3DNxFQrbSYjVMcpRXIJFThLeOZQ==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-21.2.1.tgz", + "integrity": "sha512-2QsN33fLO3N/RRFfUxDKHMX/Y/2TH90Tx51Wi6hi1do9IJdlfEe1qBw+5F0g1F1CuFEYgZWMJdZIK7LPHpuDzw==", "license": "MIT", "dependencies": { - "@babel/core": "7.28.4", + "@babel/core": "7.29.0", "@types/babel__core": "7.20.5", "tinyglobby": "^0.2.12", "yargs": "^18.0.0" @@ -781,14 +1049,14 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/compiler": "21.0.7", - "@angular/compiler-cli": "21.0.7" + "@angular/compiler": "21.2.1", + "@angular/compiler-cli": "21.2.1" } }, "node_modules/@angular/platform-browser": { - "version": "21.0.7", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.0.7.tgz", - "integrity": "sha512-mhsN2hn5qG0Oelqpko3uLmYdqadruzG2rY3CJ7duRdOrzs5g5F8QhzphoI/ljgLyxrrgZT6Nykyyf6RNhowf2A==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.1.tgz", + "integrity": "sha512-k4SJLxIaLT26vLjLuFL+ho0BiG5PrdxEsjsXFC7w5iUhomeouzkHVTZ4t7gaLNKrdRD7QNtU4Faw0nL0yx0ZPQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -797,9 +1065,9 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/animations": "21.0.7", - "@angular/common": "21.0.7", - "@angular/core": "21.0.7" + "@angular/animations": "21.2.1", + "@angular/common": "21.2.1", + "@angular/core": "21.2.1" }, "peerDependenciesMeta": { "@angular/animations": { @@ -808,9 +1076,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "21.0.7", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-21.0.7.tgz", - "integrity": "sha512-PnARi0eleIEZ/sqU286zDRLwiNI9hz16M9NRzC1kBZ+/LAj8iNWz1ZERyb4gGDOBDM/9NjdL7PU7UJvqgvvlzA==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-21.2.1.tgz", + "integrity": "sha512-J4KnrXjgSuk7KjEm79/RK1yyzR867sIyT5mcG6jx2KmkjspFJd4OeOux7Oj7lSBM7+nDEsKC9F6s0x3dC0hCPQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -819,16 +1087,16 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "21.0.7", - "@angular/compiler": "21.0.7", - "@angular/core": "21.0.7", - "@angular/platform-browser": "21.0.7" + "@angular/common": "21.2.1", + "@angular/compiler": "21.2.1", + "@angular/core": "21.2.1", + "@angular/platform-browser": "21.2.1" } }, "node_modules/@angular/router": { - "version": "21.0.7", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.0.7.tgz", - "integrity": "sha512-MBmryTBCkyc4EjfI0NWfNNTS6Dcx/yQ77hOdDrqLMdbtOtbbD9BnUXd1qRcs73s0D5Stjk1IH49D66JMKn9Xew==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.1.tgz", + "integrity": "sha512-FUKG+8ImQYxmlDUdAs7+VeS/VrBNrbo0zGiKkzVNU/bbcCyroKXJLXFtkFI3qmROiJNyIta2IMBCHJvIjLIMig==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -837,19 +1105,19 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "21.0.7", - "@angular/core": "21.0.7", - "@angular/platform-browser": "21.0.7", + "@angular/common": "21.2.1", + "@angular/core": "21.2.1", + "@angular/platform-browser": "21.2.1", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -858,29 +1126,29 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", - "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.4", - "@babel/types": "^7.28.4", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -912,13 +1180,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -941,12 +1209,12 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -975,27 +1243,27 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1045,25 +1313,25 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -1073,31 +1341,31 @@ } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -1105,9 +1373,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -1186,9 +1454,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.26.0.tgz", - "integrity": "sha512-hj0sKNCQOOo2fgyII3clmJXP28VhgDfU5iy3GNHlWO76KG6N7x4D9ezH5lJtQTG+1J6MFDAJXC1qsI+W+LvZoA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", "cpu": [ "ppc64" ], @@ -1203,9 +1471,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.26.0.tgz", - "integrity": "sha512-C0hkDsYNHZkBtPxxDx177JN90/1MiCpvBNjz1f5yWJo1+5+c5zr8apjastpEG+wtPjo9FFtGG7owSsAxyKiHxA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", "cpu": [ "arm" ], @@ -1220,9 +1488,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.26.0.tgz", - "integrity": "sha512-DDnoJ5eoa13L8zPh87PUlRd/IyFaIKOlRbxiwcSbeumcJ7UZKdtuMCHa1Q27LWQggug6W4m28i4/O2qiQQ5NZQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", "cpu": [ "arm64" ], @@ -1237,9 +1505,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.26.0.tgz", - "integrity": "sha512-bKDkGXGZnj0T70cRpgmv549x38Vr2O3UWLbjT2qmIkdIWcmlg8yebcFWoT9Dku7b5OV3UqPEuNKRzlNhjwUJ9A==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", "cpu": [ "x64" ], @@ -1254,9 +1522,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.26.0.tgz", - "integrity": "sha512-6Z3naJgOuAIB0RLlJkYc81An3rTlQ/IeRdrU3dOea8h/PvZSgitZV+thNuIccw0MuK1GmIAnAmd5TrMZad8FTQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", "cpu": [ "arm64" ], @@ -1271,9 +1539,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.26.0.tgz", - "integrity": "sha512-OPnYj0zpYW0tHusMefyaMvNYQX5pNQuSsHFTHUBNp3vVXupwqpxofcjVsUx11CQhGVkGeXjC3WLjh91hgBG2xw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", "cpu": [ "x64" ], @@ -1288,9 +1556,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.26.0.tgz", - "integrity": "sha512-jix2fa6GQeZhO1sCKNaNMjfj5hbOvoL2F5t+w6gEPxALumkpOV/wq7oUBMHBn2hY2dOm+mEV/K+xfZy3mrsxNQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", "cpu": [ "arm64" ], @@ -1305,9 +1573,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.26.0.tgz", - "integrity": "sha512-tccJaH5xHJD/239LjbVvJwf6T4kSzbk6wPFerF0uwWlkw/u7HL+wnAzAH5GB2irGhYemDgiNTp8wJzhAHQ64oA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", "cpu": [ "x64" ], @@ -1322,9 +1590,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.26.0.tgz", - "integrity": "sha512-JY8NyU31SyRmRpuc5W8PQarAx4TvuYbyxbPIpHAZdr/0g4iBr8KwQBS4kiiamGl2f42BBecHusYCsyxi7Kn8UQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", "cpu": [ "arm" ], @@ -1339,9 +1607,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.26.0.tgz", - "integrity": "sha512-IMJYN7FSkLttYyTbsbme0Ra14cBO5z47kpamo16IwggzzATFY2lcZAwkbcNkWiAduKrTgFJP7fW5cBI7FzcuNQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", "cpu": [ "arm64" ], @@ -1356,9 +1624,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.26.0.tgz", - "integrity": "sha512-XITaGqGVLgk8WOHw8We9Z1L0lbLFip8LyQzKYFKO4zFo1PFaaSKsbNjvkb7O8kEXytmSGRkYpE8LLVpPJpsSlw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", "cpu": [ "ia32" ], @@ -1373,9 +1641,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.26.0.tgz", - "integrity": "sha512-MkggfbDIczStUJwq9wU7gQ7kO33d8j9lWuOCDifN9t47+PeI+9m2QVh51EI/zZQ1spZtFMC1nzBJ+qNGCjJnsg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", "cpu": [ "loong64" ], @@ -1390,9 +1658,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.26.0.tgz", - "integrity": "sha512-fUYup12HZWAeccNLhQ5HwNBPr4zXCPgUWzEq2Rfw7UwqwfQrFZ0SR/JljaURR8xIh9t+o1lNUFTECUTmaP7yKA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", "cpu": [ "mips64el" ], @@ -1407,9 +1675,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.26.0.tgz", - "integrity": "sha512-MzRKhM0Ip+//VYwC8tialCiwUQ4G65WfALtJEFyU0GKJzfTYoPBw5XNWf0SLbCUYQbxTKamlVwPmcw4DgZzFxg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", "cpu": [ "ppc64" ], @@ -1424,9 +1692,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.26.0.tgz", - "integrity": "sha512-QhCc32CwI1I4Jrg1enCv292sm3YJprW8WHHlyxJhae/dVs+KRWkbvz2Nynl5HmZDW/m9ZxrXayHzjzVNvQMGQA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", "cpu": [ "riscv64" ], @@ -1441,9 +1709,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.26.0.tgz", - "integrity": "sha512-1D6vi6lfI18aNT1aTf2HV+RIlm6fxtlAp8eOJ4mmnbYmZ4boz8zYDar86sIYNh0wmiLJEbW/EocaKAX6Yso2fw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", "cpu": [ "s390x" ], @@ -1458,9 +1726,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.26.0.tgz", - "integrity": "sha512-rnDcepj7LjrKFvZkx+WrBv6wECeYACcFjdNPvVPojCPJD8nHpb3pv3AuR9CXgdnjH1O23btICj0rsp0L9wAnHA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", "cpu": [ "x64" ], @@ -1475,9 +1743,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.26.0.tgz", - "integrity": "sha512-FSWmgGp0mDNjEXXFcsf12BmVrb+sZBBBlyh3LwB/B9ac3Kkc8x5D2WimYW9N7SUkolui8JzVnVlWh7ZmjCpnxw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", "cpu": [ "arm64" ], @@ -1492,9 +1760,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.26.0.tgz", - "integrity": "sha512-0QfciUDFryD39QoSPUDshj4uNEjQhp73+3pbSAaxjV2qGOEDsM67P7KbJq7LzHoVl46oqhIhJ1S+skKGR7lMXA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", "cpu": [ "x64" ], @@ -1509,9 +1777,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.26.0.tgz", - "integrity": "sha512-vmAK+nHhIZWImwJ3RNw9hX3fU4UGN/OqbSE0imqljNbUQC3GvVJ1jpwYoTfD6mmXmQaxdJY6Hn4jQbLGJKg5Yw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", "cpu": [ "arm64" ], @@ -1526,9 +1794,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.26.0.tgz", - "integrity": "sha512-GPXF7RMkJ7o9bTyUsnyNtrFMqgM3X+uM/LWw4CeHIjqc32fm0Ir6jKDnWHpj8xHFstgWDUYseSABK9KCkHGnpg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", "cpu": [ "x64" ], @@ -1543,9 +1811,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.26.0.tgz", - "integrity": "sha512-nUHZ5jEYqbBthbiBksbmHTlbb5eElyVfs/s1iHQ8rLBq1eWsd5maOnDpCocw1OM8kFK747d1Xms8dXJHtduxSw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", "cpu": [ "arm64" ], @@ -1560,9 +1828,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.26.0.tgz", - "integrity": "sha512-TMg3KCTCYYaVO+R6P5mSORhcNDDlemUVnUbb8QkboUtOhb5JWKAzd5uMIMECJQOxHZ/R+N8HHtDF5ylzLfMiLw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", "cpu": [ "x64" ], @@ -1577,9 +1845,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.26.0.tgz", - "integrity": "sha512-apqYgoAUd6ZCb9Phcs8zN32q6l0ZQzQBdVXOofa6WvHDlSOhwCWgSfVQabGViThS40Y1NA4SCvQickgZMFZRlA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", "cpu": [ "arm64" ], @@ -1594,9 +1862,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.26.0.tgz", - "integrity": "sha512-FGJAcImbJNZzLWu7U6WB0iKHl4RuY4TsXEwxJPl9UZLS47agIZuILZEX3Pagfw7I4J3ddflomt9f0apfaJSbaw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", "cpu": [ "ia32" ], @@ -1611,9 +1879,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.26.0.tgz", - "integrity": "sha512-WAckBKaVnmFqbEhbymrPK7M086DQMpL1XoRbpmN0iW8k5JSXjDRQBhcZNa0VweItknLq9eAeCL34jK7/CDcw7A==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", "cpu": [ "x64" ], @@ -1657,202 +1925,104 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.2.tgz", + "integrity": "sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.7", + "@eslint/object-schema": "^3.0.2", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^10.2.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.2.tgz", + "integrity": "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0" + "@eslint/core": "^1.1.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.0.tgz", + "integrity": "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.2.tgz", + "integrity": "sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.0.tgz", + "integrity": "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0", + "@eslint/core": "^1.1.0", "levn": "^0.4.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@fortawesome/fontawesome-free": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.1.0.tgz", - "integrity": "sha512-+WxNld5ZCJHvPQCr/GnzCTVREyStrAJjisUPtUxG5ngDA8TMlPnKp6dddlTpai4+1GNmltAeuk1hJEkBohwZYA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.2.0.tgz", + "integrity": "sha512-3DguDv/oUE+7vjMeTSOjCSG+KeawgVQOHrKRnvUuqYh1mfArrh7s+s8hXW3e4RerBA1+Wh+hBqf8sJNpqNrBWg==", "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", "engines": { "node": ">=6" } }, + "node_modules/@gar/promise-retry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.2.tgz", + "integrity": "sha512-Lm/ZLhDZcBECta3TmCQSngiQykFdfw+QtI1/GYMsZd4l3nG+P8WLB16XuS7WaBGLQ+9E+cOcWQsth9cayuGt8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "retry": "^0.13.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@harperfast/extended-iterable": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@harperfast/extended-iterable/-/extended-iterable-1.0.3.tgz", + "integrity": "sha512-sSAYhQca3rDWtQUHSAPeO7axFIUJOI6hn1gjRC5APVE1a90tuyT8f5WIgRsFhhWA7htNkju2veB9eWL6YHi/Lw==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, "node_modules/@hono/node-server": { - "version": "1.19.7", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz", - "integrity": "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==", + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", "dev": true, "license": "MIT", "engines": { @@ -1915,9 +2085,9 @@ } }, "node_modules/@iharbeck/ngx-virtual-scroller": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/@iharbeck/ngx-virtual-scroller/-/ngx-virtual-scroller-19.0.1.tgz", - "integrity": "sha512-dtn4CpbEY92H9nd1A48WNhsyUgtFBjC83xcsc9VzlSQT/KN2fEx0oBs0Obnn6ZdPanDP/IQdlBgmANmlds/wHA==", + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@iharbeck/ngx-virtual-scroller/-/ngx-virtual-scroller-20.0.0.tgz", + "integrity": "sha512-D78O3XPLzQrIZAnJ797rTyoyiUJdw/V71yj9E21gEQ3Gt6Ykt4hmYp+X6+b4llJ6rQyp3maIEEru5sp8UE6HCw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -1962,14 +2132,14 @@ } }, "node_modules/@inquirer/confirm": { - "version": "5.1.19", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.19.tgz", - "integrity": "sha512-wQNz9cfcxrtEnUyG5PndC8g3gZ7lGDBzmWiXZkX8ot3vfZ+/BLjR8EvyGX4YzQLeVqtAlY/YScZpW7CW8qMoDQ==", + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.0", - "@inquirer/type": "^3.0.9" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { "node": ">=18" @@ -2157,22 +2327,22 @@ } }, "node_modules/@inquirer/prompts": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.9.0.tgz", - "integrity": "sha512-X7/+dG9SLpSzRkwgG5/xiIzW0oMrV3C0HOa7YHG1WnrLK+vCQHfte4k/T80059YBdei29RBC3s+pSMvPJDU9/A==", + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/checkbox": "^4.3.0", - "@inquirer/confirm": "^5.1.19", - "@inquirer/editor": "^4.2.21", - "@inquirer/expand": "^4.0.21", - "@inquirer/input": "^4.2.5", - "@inquirer/number": "^3.0.21", - "@inquirer/password": "^4.0.21", - "@inquirer/rawlist": "^4.1.9", - "@inquirer/search": "^3.2.0", - "@inquirer/select": "^4.4.0" + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" }, "engines": { "node": ">=18" @@ -2306,29 +2476,6 @@ "rxjs": "^7.0.0" } }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -2398,12 +2545,12 @@ } }, "node_modules/@jsverse/transloco": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@jsverse/transloco/-/transloco-8.2.0.tgz", - "integrity": "sha512-5SU9mjmKHlTraW/GKSUsWEjt7ATBLzKcKd6w+mTbRrnU38ZyYdCJoR2W/ii8lWiRwhfgbXTFCsTUueW5Ak61WA==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@jsverse/transloco/-/transloco-8.2.1.tgz", + "integrity": "sha512-uuapT1vNi/P9wqklO2VY/sIj8HPVQJ1h+IJFhPbiQvk1FP/vgn2LLwGz/iIcet2bAMJVKKxO8FXytdrwRXXyvg==", "license": "MIT", "dependencies": { - "@jsverse/transloco-utils": "^8.2.0", + "@jsverse/transloco-utils": "^8.2.1", "@jsverse/utils": "1.0.0-beta.5", "tslib": "^2.2.0" }, @@ -2413,9 +2560,9 @@ } }, "node_modules/@jsverse/transloco-locale": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@jsverse/transloco-locale/-/transloco-locale-8.2.0.tgz", - "integrity": "sha512-EMj9f1ugqKT0m6V3heTrJ4dm9UV5vNiLj3WnMKWoiNfqsZtUr6FTeTsTNoDCBSel4ucC9pCVfmcFk6SUUzfIAQ==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@jsverse/transloco-locale/-/transloco-locale-8.2.1.tgz", + "integrity": "sha512-EyfFKLLp4c4PKYJcON7UyguF/VYH7LlcGSwkissNaqqlaCPQASeM188kpvLfn2inekX26UAj59lCXQpsHPEdYA==", "license": "MIT", "dependencies": { "tslib": "^2.2.0" @@ -2427,9 +2574,9 @@ } }, "node_modules/@jsverse/transloco-persist-lang": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@jsverse/transloco-persist-lang/-/transloco-persist-lang-8.2.0.tgz", - "integrity": "sha512-accsQa5eFgR4yv+v7Uv5gydexb8jHIKymP/tYzGqOavpThkqUzlbVS1A8VhhsiB98w3FPy7lx+pn+pSCye7noA==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@jsverse/transloco-persist-lang/-/transloco-persist-lang-8.2.1.tgz", + "integrity": "sha512-7M/uvPFvOq2pCMIQHD20o8DnsrCIFncc2J98U+DNPn/DatgZRn+YJVjJ697Y8MzRnvjJF4D7YoZvQ+GzZ5ka8Q==", "license": "MIT", "dependencies": { "tslib": "^2.2.0" @@ -2441,9 +2588,9 @@ } }, "node_modules/@jsverse/transloco-persist-translations": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@jsverse/transloco-persist-translations/-/transloco-persist-translations-8.2.0.tgz", - "integrity": "sha512-UF443fwRnhjYAWuhedyGNpUnHPuVH+vN0zNMJKi/WpGI3gZa+VINaIDANJVtJ0jQcY4ONx5dP3P71j1XL2jfnw==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@jsverse/transloco-persist-translations/-/transloco-persist-translations-8.2.1.tgz", + "integrity": "sha512-oSeKTKnmh1eloX5Au6ic/dFiGV/X6RfB69v5FujOIdR/3yvIos5cNE64F49Jmgdfq89UePyTLu0k+H0q5Awu+w==", "license": "MIT", "dependencies": { "tslib": "^2.2.0" @@ -2455,9 +2602,9 @@ } }, "node_modules/@jsverse/transloco-preload-langs": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@jsverse/transloco-preload-langs/-/transloco-preload-langs-8.2.0.tgz", - "integrity": "sha512-O8VH8cDoeHIxj9+1reagOPk7FCSFK04iRbyKsPIlJSkhXBzAc9mbwykwZ+Aa3Wt7GWeRo75Tp/sMDaMIFzCXhA==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@jsverse/transloco-preload-langs/-/transloco-preload-langs-8.2.1.tgz", + "integrity": "sha512-YSfq7FwYeDXBUVSXhmhZfswKZxlqLevicpnnO2L2pxxDEzYWa8+poD5J4IehtexaXEyisJXOvOMJH/yaAJSGtg==", "license": "MIT", "dependencies": { "tslib": "^2.2.0" @@ -2468,9 +2615,9 @@ } }, "node_modules/@jsverse/transloco-utils": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@jsverse/transloco-utils/-/transloco-utils-8.2.0.tgz", - "integrity": "sha512-rDactF2Qmu4JKBpecyYLzD3spPZ0U+6wgoQS2OIcVraq5riV8eE3sPYb5dgL2wxMgGtJRuT8PgMMAD7LUOcCNw==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@jsverse/transloco-utils/-/transloco-utils-8.2.1.tgz", + "integrity": "sha512-sAKJQuGgAYRYwndM8X1xVbwOrjENBxKxOwhXE7gFnS8fWUEwBGMswp3wbAOS5jZlLDhyaReesU16ToXLegBCjg==", "license": "MIT", "dependencies": { "cosmiconfig": "^8.1.3", @@ -2504,9 +2651,9 @@ } }, "node_modules/@lmdb/lmdb-darwin-arm64": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.4.3.tgz", - "integrity": "sha512-zR6Y45VNtW5s+A+4AyhrJk0VJKhXdkLhrySCpCu7PSdnakebsOzNxf58p5Xoq66vOSuueGAxlqDAF49HwdrSTQ==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.5.1.tgz", + "integrity": "sha512-tpfN4kKrrMpQ+If1l8bhmoNkECJi0iOu6AEdrTJvWVC+32sLxTARX5Rsu579mPImRP9YFWfWgeRQ5oav7zApQQ==", "cpu": [ "arm64" ], @@ -2518,9 +2665,9 @@ ] }, "node_modules/@lmdb/lmdb-darwin-x64": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.4.3.tgz", - "integrity": "sha512-nfGm5pQksBGfaj9uMbjC0YyQreny/Pl7mIDtHtw6g7WQuCgeLullr9FNRsYyKplaEJBPrCVpEjpAznxTBIrXBw==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.5.1.tgz", + "integrity": "sha512-+a2tTfc3rmWhLAolFUWRgJtpSuu+Fw/yjn4rF406NMxhfjbMuiOUTDRvRlMFV+DzyjkwnokisskHbCWkS3Ly5w==", "cpu": [ "x64" ], @@ -2532,9 +2679,9 @@ ] }, "node_modules/@lmdb/lmdb-linux-arm": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.4.3.tgz", - "integrity": "sha512-Kjqomp7i0rgSbYSUmv9JnXpS55zYT/YcW3Bdf9oqOTjcH0/8tFAP8MLhu/i9V2pMKIURDZk63Ww49DTK0T3c/Q==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.5.1.tgz", + "integrity": "sha512-0EgcE6reYr8InjD7V37EgXcYrloqpxVPINy3ig1MwDSbl6LF/vXTYRH9OE1Ti1D8YZnB35ZH9aTcdfSb5lql2A==", "cpu": [ "arm" ], @@ -2546,9 +2693,9 @@ ] }, "node_modules/@lmdb/lmdb-linux-arm64": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.4.3.tgz", - "integrity": "sha512-uX9eaPqWb740wg5D3TCvU/js23lSRSKT7lJrrQ8IuEG/VLgpPlxO3lHDywU44yFYdGS7pElBn6ioKFKhvALZlw==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.5.1.tgz", + "integrity": "sha512-aoERa5B6ywXdyFeYGQ1gbQpkMkDbEo45qVoXE5QpIRavqjnyPwjOulMkmkypkmsbJ5z4Wi0TBztON8agCTG0Vg==", "cpu": [ "arm64" ], @@ -2560,9 +2707,9 @@ ] }, "node_modules/@lmdb/lmdb-linux-x64": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.4.3.tgz", - "integrity": "sha512-7/8l20D55CfwdMupkc3fNxNJdn4bHsti2X0cp6PwiXlLeSFvAfWs5kCCx+2Cyje4l4GtN//LtKWjTru/9hDJQg==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.5.1.tgz", + "integrity": "sha512-SqNDY1+vpji7bh0sFH5wlWyFTOzjbDOl0/kB5RLLYDAFyd/uw3n7wyrmas3rYPpAW7z18lMOi1yKlTPv967E3g==", "cpu": [ "x64" ], @@ -2574,9 +2721,9 @@ ] }, "node_modules/@lmdb/lmdb-win32-arm64": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.4.3.tgz", - "integrity": "sha512-yWVR0e5Gl35EGJBsAuqPOdjtUYuN8CcTLKrqpQFoM+KsMadViVCulhKNhkcjSGJB88Am5bRPjMro4MBB9FS23Q==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.5.1.tgz", + "integrity": "sha512-50v0O1Lt37cwrmR9vWZK5hRW0Aw+KEmxJJ75fge/zIYdvNKB/0bSMSVR5Uc2OV9JhosIUyklOmrEvavwNJ8D6w==", "cpu": [ "arm64" ], @@ -2588,9 +2735,9 @@ ] }, "node_modules/@lmdb/lmdb-win32-x64": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.4.3.tgz", - "integrity": "sha512-1JdBkcO0Vrua4LUgr4jAe4FUyluwCeq/pDkBrlaVjX3/BBWP1TzVjCL+TibWNQtPAL1BITXPAhlK5Ru4FBd/hg==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.5.1.tgz", + "integrity": "sha512-qwosvPyl+zpUlp3gRb7UcJ3H8S28XHCzkv0Y0EgQToXjQP91ZD67EHSCDmaLjtKhe+GVIW5om1KUpzVLA0l6pg==", "cpu": [ "x64" ], @@ -2615,13 +2762,13 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.25.2", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.2.tgz", - "integrity": "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==", + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", "dev": true, "license": "MIT", "dependencies": { - "@hono/node-server": "^1.19.7", + "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", @@ -2629,14 +2776,15 @@ "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "jose": "^6.1.1", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.0" + "zod-to-json-schema": "^3.25.1" }, "engines": { "node": ">=18" @@ -3126,9 +3274,9 @@ } }, "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -3149,18 +3297,18 @@ } }, "node_modules/@npmcli/git": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.1.tgz", - "integrity": "sha512-+XTFxK2jJF/EJJ5SoAzXk3qwIDfvFc5/g+bD274LZ7uY7LE8sTfG6Z8rOanPl2ZEvZWqNvmEdtXC25cE54VcoA==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.2.tgz", + "integrity": "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==", "dev": true, "license": "ISC", "dependencies": { + "@gar/promise-retry": "^1.0.0", "@npmcli/promise-spawn": "^9.0.0", "ini": "^6.0.0", "lru-cache": "^11.2.1", "npm-pick-manifest": "^11.0.1", "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", "semver": "^7.3.5", "which": "^6.0.0" }, @@ -3168,67 +3316,34 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@npmcli/git/node_modules/@npmcli/promise-spawn": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", - "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "which": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/git/node_modules/ini": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", - "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/@npmcli/git/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=16" + "node": ">=20" } }, "node_modules/@npmcli/git/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, - "node_modules/@npmcli/git/node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/@npmcli/git/node_modules/which": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", - "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", "dev": true, "license": "ISC", "dependencies": { - "isexe": "^3.1.1" + "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" @@ -3238,20 +3353,20 @@ } }, "node_modules/@npmcli/installed-package-contents": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-3.0.0.tgz", - "integrity": "sha512-fkxoPuFGvxyrH+OQzyTkX2LUEamrF4jZSmxjAtPPHHGO0dqsQ8tTKjnIS8SAnPHdk2I03BDtSMR5K/4loKg79Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-4.0.0.tgz", + "integrity": "sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==", "dev": true, "license": "ISC", "dependencies": { - "npm-bundled": "^4.0.0", - "npm-normalize-package-bin": "^4.0.0" + "npm-bundled": "^5.0.0", + "npm-normalize-package-bin": "^5.0.0" }, "bin": { "installed-package-contents": "bin/index.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/node-gyp": { @@ -3265,9 +3380,9 @@ } }, "node_modules/@npmcli/package-json": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.4.tgz", - "integrity": "sha512-0wInJG3j/K40OJt/33ax47WfWMzZTm6OQxB9cDhTt5huCP2a9g2GnlsxmfN+PulItNPIpPrZ+kfwwUil7eHcZQ==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.5.tgz", + "integrity": "sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ==", "dev": true, "license": "ISC", "dependencies": { @@ -3277,59 +3392,49 @@ "json-parse-even-better-errors": "^5.0.0", "proc-log": "^6.0.0", "semver": "^7.5.3", - "validate-npm-package-license": "^3.0.4" + "spdx-expression-parse": "^4.0.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@npmcli/package-json/node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/@npmcli/promise-spawn": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-8.0.3.tgz", - "integrity": "sha512-Yb00SWaL4F8w+K8YGhQ55+xE4RUNdMHV43WZGsiTM92gS+lC0mGsn7I4hLug7pbao035S6bj3Y3w0cUNGLfmkg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", + "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", "dev": true, "license": "ISC", "dependencies": { - "which": "^5.0.0" + "which": "^6.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/promise-spawn/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=16" + "node": ">=20" } }, "node_modules/@npmcli/promise-spawn/node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", "dev": true, "license": "ISC", "dependencies": { - "isexe": "^3.1.1" + "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/redact": { @@ -3343,9 +3448,9 @@ } }, "node_modules/@npmcli/run-script": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.3.tgz", - "integrity": "sha512-ER2N6itRkzWbbtVmZ9WKaWxVlKlOeBFF1/7xx+KA5J1xKa4JjUwBdb6tDpk0v1qA+d+VDwHI9qmLcXSWcmi+Rw==", + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.4.tgz", + "integrity": "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==", "dev": true, "license": "ISC", "dependencies": { @@ -3353,66 +3458,16 @@ "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^9.0.0", "node-gyp": "^12.1.0", - "proc-log": "^6.0.0", - "which": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/run-script/node_modules/@npmcli/promise-spawn": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", - "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "which": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/run-script/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16" - } - }, - "node_modules/@npmcli/run-script/node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/run-script/node_modules/which": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", - "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" + "proc-log": "^6.0.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@oxc-project/types": { - "version": "0.96.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.96.0.tgz", - "integrity": "sha512-r/xkmoXA0xEpU6UGtn18CNVjXH6erU3KCpCDbpLmbVxBFor1U9MqN5Z2uMmCHJuXjJzlnDR+hWY+yPoLo8oHDw==", + "version": "0.113.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.113.0.tgz", + "integrity": "sha512-Tp3XmgxwNQ9pEN9vxgJBAqdRamHibi76iowQ38O2I4PMpcvNRQNVsU2n1x1nv9yh0XoTrGFzf7cZSGxmixxrhA==", "dev": true, "license": "MIT", "funding": { @@ -3420,18 +3475,18 @@ } }, "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, "dependencies": { - "detect-libc": "^1.0.3", + "detect-libc": "^2.0.3", "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">= 10.0.0" @@ -3441,25 +3496,25 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" } }, "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", "cpu": [ "arm64" ], @@ -3478,9 +3533,9 @@ } }, "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", "cpu": [ "arm64" ], @@ -3499,9 +3554,9 @@ } }, "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", "cpu": [ "x64" ], @@ -3520,9 +3575,9 @@ } }, "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", "cpu": [ "x64" ], @@ -3541,9 +3596,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", "cpu": [ "arm" ], @@ -3562,9 +3617,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", "cpu": [ "arm" ], @@ -3583,9 +3638,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", "cpu": [ "arm64" ], @@ -3604,9 +3659,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", "cpu": [ "arm64" ], @@ -3625,9 +3680,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", "cpu": [ "x64" ], @@ -3646,9 +3701,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", "cpu": [ "x64" ], @@ -3667,9 +3722,9 @@ } }, "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", "cpu": [ "arm64" ], @@ -3688,9 +3743,9 @@ } }, "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", "cpu": [ "ia32" ], @@ -3709,9 +3764,9 @@ } }, "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", "cpu": [ "x64" ], @@ -3729,20 +3784,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher/node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/@parcel/watcher/node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", @@ -3769,9 +3810,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.47.tgz", - "integrity": "sha512-vPP9/MZzESh9QtmvQYojXP/midjgkkc1E4AdnPPAzQXo668ncHJcVLKjJKzoBdsQmaIvNjrMdsCwES8vTQHRQw==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.4.tgz", + "integrity": "sha512-vRq9f4NzvbdZavhQbjkJBx7rRebDKYR9zHfO/Wg486+I7bSecdUapzCm5cyXoK+LHokTxgSq7A5baAXUZkIz0w==", "cpu": [ "arm64" ], @@ -3786,9 +3827,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.47.tgz", - "integrity": "sha512-Lc3nrkxeaDVCVl8qR3qoxh6ltDZfkQ98j5vwIr5ALPkgjZtDK4BGCrrBoLpGVMg+csWcaqUbwbKwH5yvVa0oOw==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.4.tgz", + "integrity": "sha512-kFgEvkWLqt3YCgKB5re9RlIrx9bRsvyVUnaTakEpOPuLGzLpLapYxE9BufJNvPg8GjT6mB1alN4yN1NjzoeM8Q==", "cpu": [ "arm64" ], @@ -3803,9 +3844,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.47.tgz", - "integrity": "sha512-eBYxQDwP0O33plqNVqOtUHqRiSYVneAknviM5XMawke3mwMuVlAsohtOqEjbCEl/Loi/FWdVeks5WkqAkzkYWQ==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.4.tgz", + "integrity": "sha512-JXmaOJGsL/+rsmMfutcDjxWM2fTaVgCHGoXS7nE8Z3c9NAYjGqHvXrAhMUZvMpHS/k7Mg+X7n/MVKb7NYWKKww==", "cpu": [ "x64" ], @@ -3820,9 +3861,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.47.tgz", - "integrity": "sha512-Ns+kgp2+1Iq/44bY/Z30DETUSiHY7ZuqaOgD5bHVW++8vme9rdiWsN4yG4rRPXkdgzjvQ9TDHmZZKfY4/G11AA==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.4.tgz", + "integrity": "sha512-ep3Catd6sPnHTM0P4hNEvIv5arnDvk01PfyJIJ+J3wVCG1eEaPo09tvFqdtcaTrkwQy0VWR24uz+cb4IsK53Qw==", "cpu": [ "x64" ], @@ -3837,9 +3878,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.47.tgz", - "integrity": "sha512-4PecgWCJhTA2EFOlptYJiNyVP2MrVP4cWdndpOu3WmXqWqZUmSubhb4YUAIxAxnXATlGjC1WjxNPhV7ZllNgdA==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.4.tgz", + "integrity": "sha512-LwA5ayKIpnsgXJEwWc3h8wPiS33NMIHd9BhsV92T8VetVAbGe2qXlJwNVDGHN5cOQ22R9uYvbrQir2AB+ntT2w==", "cpu": [ "arm" ], @@ -3854,9 +3895,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.47.tgz", - "integrity": "sha512-CyIunZ6D9U9Xg94roQI1INt/bLkOpPsZjZZkiaAZ0r6uccQdICmC99M9RUPlMLw/qg4yEWLlQhG73W/mG437NA==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.4.tgz", + "integrity": "sha512-AC1WsGdlV1MtGay/OQ4J9T7GRadVnpYRzTcygV1hKnypbYN20Yh4t6O1Sa2qRBMqv1etulUknqXjc3CTIsBu6A==", "cpu": [ "arm64" ], @@ -3871,9 +3912,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.47.tgz", - "integrity": "sha512-doozc/Goe7qRCSnzfJbFINTHsMktqmZQmweull6hsZZ9sjNWQ6BWQnbvOlfZJe4xE5NxM1NhPnY5Giqnl3ZrYQ==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.4.tgz", + "integrity": "sha512-lU+6rgXXViO61B4EudxtVMXSOfiZONR29Sys5VGSetUY7X8mg9FCKIIjcPPj8xNDeYzKl+H8F/qSKOBVFJChCQ==", "cpu": [ "arm64" ], @@ -3888,9 +3929,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.47.tgz", - "integrity": "sha512-fodvSMf6Aqwa0wEUSTPewmmZOD44rc5Tpr5p9NkwQ6W1SSpUKzD3SwpJIgANDOhwiYhDuiIaYPGB7Ujkx1q0UQ==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.4.tgz", + "integrity": "sha512-DZaN1f0PGp/bSvKhtw50pPsnln4T13ycDq1FrDWRiHmWt1JeW+UtYg9touPFf8yt993p8tS2QjybpzKNTxYEwg==", "cpu": [ "x64" ], @@ -3905,9 +3946,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.47.tgz", - "integrity": "sha512-Rxm5hYc0mGjwLh5sjlGmMygxAaV2gnsx7CNm2lsb47oyt5UQyPDZf3GP/ct8BEcwuikdqzsrrlIp8+kCSvMFNQ==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.4.tgz", + "integrity": "sha512-RnGxwZLN7fhMMAItnD6dZ7lvy+TI7ba+2V54UF4dhaWa/p8I/ys1E73KO6HmPmgz92ZkfD8TXS1IMV8+uhbR9g==", "cpu": [ "x64" ], @@ -3922,9 +3963,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.47.tgz", - "integrity": "sha512-YakuVe+Gc87jjxazBL34hbr8RJpRuFBhun7NEqoChVDlH5FLhLXjAPHqZd990TVGVNkemourf817Z8u2fONS8w==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.4.tgz", + "integrity": "sha512-6lcI79+X8klGiGd8yHuTgQRjuuJYNggmEml+RsyN596P23l/zf9FVmJ7K0KVKkFAeYEdg0iMUKyIxiV5vebDNQ==", "cpu": [ "arm64" ], @@ -3939,9 +3980,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.47.tgz", - "integrity": "sha512-ak2GvTFQz3UAOw8cuQq8pWE+TNygQB6O47rMhvevvTzETh7VkHRFtRUwJynX5hwzFvQMP6G0az5JrBGuwaMwYQ==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.4.tgz", + "integrity": "sha512-wz7ohsKCAIWy91blZ/1FlpPdqrsm1xpcEOQVveWoL6+aSPKL4VUcoYmmzuLTssyZxRpEwzuIxL/GDsvpjaBtOw==", "cpu": [ "wasm32" ], @@ -3949,16 +3990,16 @@ "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^1.0.7" + "@napi-rs/wasm-runtime": "^1.1.1" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.47.tgz", - "integrity": "sha512-o5BpmBnXU+Cj+9+ndMcdKjhZlPb79dVPBZnWwMnI4RlNSSq5yOvFZqvfPYbyacvnW03Na4n5XXQAPhu3RydZ0w==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.4.tgz", + "integrity": "sha512-cfiMrfuWCIgsFmcVG0IPuO6qTRHvF7NuG3wngX1RZzc6dU8FuBFb+J3MIR5WrdTNozlumfgL4cvz+R4ozBCvsQ==", "cpu": [ "arm64" ], @@ -3972,27 +4013,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-win32-ia32-msvc": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.47.tgz", - "integrity": "sha512-FVOmfyYehNE92IfC9Kgs913UerDog2M1m+FADJypKz0gmRg3UyTt4o1cZMCAl7MiR89JpM9jegNO1nXuP1w1vw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.47.tgz", - "integrity": "sha512-by/70F13IUE101Bat0oeH8miwWX5mhMFPk1yjCdxoTNHTyTdLgb0THNaebRM6AP7Kz+O3O2qx87sruYuF5UxHg==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.4.tgz", + "integrity": "sha512-p6UeR9y7ht82AH57qwGuFYn69S6CZ7LLKdCKy/8T3zS9VTrJei2/CGsTUV45Da4Z9Rbhc7G4gyWQ/Ioamqn09g==", "cpu": [ "x64" ], @@ -4007,16 +4031,16 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", - "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.4.tgz", + "integrity": "sha512-1BrrmTu0TWfOP1riA8uakjFc9bpIUGzVKETsOtzY39pPga8zELGDl8eu1Dx7/gjM5CAz14UknsUMpBO8L+YntQ==", "dev": true, "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", - "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], @@ -4028,9 +4052,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", - "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], @@ -4042,9 +4066,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", - "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], @@ -4056,9 +4080,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", - "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], @@ -4070,9 +4094,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", - "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], @@ -4084,9 +4108,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", - "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], @@ -4098,9 +4122,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", - "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], @@ -4112,9 +4136,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", - "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], @@ -4126,9 +4150,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", - "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], @@ -4140,9 +4164,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", - "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], @@ -4154,9 +4178,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", - "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", "cpu": [ "loong64" ], @@ -4168,9 +4192,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", - "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], @@ -4182,9 +4206,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", - "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", "cpu": [ "ppc64" ], @@ -4196,9 +4220,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", - "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], @@ -4210,9 +4234,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", - "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], @@ -4224,9 +4248,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", - "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], @@ -4238,9 +4262,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", - "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], @@ -4252,9 +4276,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", - "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], @@ -4266,9 +4290,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", - "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], @@ -4280,9 +4304,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", - "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", "cpu": [ "x64" ], @@ -4294,9 +4318,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", - "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "cpu": [ "arm64" ], @@ -4308,9 +4332,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", - "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], @@ -4322,9 +4346,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", - "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], @@ -4336,9 +4360,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", - "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", "cpu": [ "x64" ], @@ -4350,9 +4374,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", - "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], @@ -4364,14 +4388,14 @@ ] }, "node_modules/@schematics/angular": { - "version": "21.0.5", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.0.5.tgz", - "integrity": "sha512-uNBIilq5bGnln3D7Nbm3/K+Ot++eGj4rygU0DCw//IZiTQU/iSyF3UAsN++iRetu/OMs+97T/RoGPjD22ryiZg==", + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.1.tgz", + "integrity": "sha512-DjrHRMoILhbZ6tc7aNZWuHA1wCm1iU/JN1TxAwNEyIBgyU3Fx8Z5baK4w0TCpOIPt0RLWVgP2L7kka9aXWCUFA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.0.5", - "@angular-devkit/schematics": "21.0.5", + "@angular-devkit/core": "21.2.1", + "@angular-devkit/schematics": "21.2.1", "jsonc-parser": "3.3.1" }, "engines": { @@ -4380,6 +4404,85 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.1.tgz", + "integrity": "sha512-TpXGjERqVPN8EPt7LdmWAwh0oNQ/6uWFutzGZiXhJy81n1zb1O1XrqhRAmvP1cAo5O+na6IV2JkkCmxL6F8GUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.3", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@schematics/angular/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@schematics/angular/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@schematics/angular/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@siemens/ngx-datatable": { "version": "25.0.0", "resolved": "https://registry.npmjs.org/@siemens/ngx-datatable/-/ngx-datatable-25.0.0.tgz", @@ -4446,16 +4549,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@sigstore/sign/node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/@sigstore/tuf": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.1.tgz", @@ -4543,22 +4636,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@tufjs/models/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@tweenjs/tween.js": { "version": "25.0.0", "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz", @@ -4910,6 +4987,13 @@ "@types/zrender": "*" } }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -4953,13 +5037,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", - "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "version": "25.3.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz", + "integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/zrender": { @@ -4969,17 +5053,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.52.0.tgz", - "integrity": "sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.52.0", - "@typescript-eslint/type-utils": "8.52.0", - "@typescript-eslint/utils": "8.52.0", - "@typescript-eslint/visitor-keys": "8.52.0", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -4992,22 +5076,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.52.0", - "eslint": "^8.57.0 || ^9.0.0", + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.52.0.tgz", - "integrity": "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.52.0", - "@typescript-eslint/types": "8.52.0", - "@typescript-eslint/typescript-estree": "8.52.0", - "@typescript-eslint/visitor-keys": "8.52.0", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "engines": { @@ -5018,19 +5102,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.52.0.tgz", - "integrity": "sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.52.0", - "@typescript-eslint/types": "^8.52.0", + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "engines": { @@ -5045,14 +5129,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.52.0.tgz", - "integrity": "sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.52.0", - "@typescript-eslint/visitor-keys": "8.52.0" + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5063,9 +5147,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.52.0.tgz", - "integrity": "sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", "dev": true, "license": "MIT", "engines": { @@ -5080,15 +5164,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.52.0.tgz", - "integrity": "sha512-JD3wKBRWglYRQkAtsyGz1AewDu3mTc7NtRjR/ceTyGoPqmdS5oCdx/oZMWD5Zuqmo6/MpsYs0wp6axNt88/2EQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.52.0", - "@typescript-eslint/typescript-estree": "8.52.0", - "@typescript-eslint/utils": "8.52.0", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -5100,14 +5184,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.52.0.tgz", - "integrity": "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", "dev": true, "license": "MIT", "engines": { @@ -5119,18 +5203,18 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.52.0.tgz", - "integrity": "sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.52.0", - "@typescript-eslint/tsconfig-utils": "8.52.0", - "@typescript-eslint/types": "8.52.0", - "@typescript-eslint/visitor-keys": "8.52.0", + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", - "minimatch": "^9.0.5", + "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" @@ -5147,16 +5231,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.52.0.tgz", - "integrity": "sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.52.0", - "@typescript-eslint/types": "8.52.0", - "@typescript-eslint/typescript-estree": "8.52.0" + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5166,19 +5250,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.52.0.tgz", - "integrity": "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.52.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5189,22 +5273,22 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/@vitejs/plugin-basic-ssl": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.0.tgz", - "integrity": "sha512-dOxxrhgyDIEUADhb/8OlV9JIqYLgos03YorAueTIeOUskLJSEsfwCByjbu98ctXitUN3znXKp0bYD/WHSudCeA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.4.tgz", + "integrity": "sha512-HXciTXN/sDBYWgeAD4V4s0DN0g72x5mlxQhHxtYu3Tt8BLa6MzcJZUyDVFCdtjNs3bfENVHVzOsmooTVuNgAAw==", "dev": true, "license": "MIT", "engines": { @@ -5258,9 +5342,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -5345,26 +5429,26 @@ } }, "node_modules/algoliasearch": { - "version": "5.40.1", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.40.1.tgz", - "integrity": "sha512-iUNxcXUNg9085TJx0HJLjqtDE0r1RZ0GOGrt8KNQqQT5ugu8lZsHuMUYW/e0lHhq6xBvmktU9Bw4CXP9VQeKrg==", + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.48.1.tgz", + "integrity": "sha512-Rf7xmeuIo7nb6S4mp4abW2faW8DauZyE2faBIKFaUfP3wnpOvNSbiI5AwVhqBNj0jPgBWEvhyCu0sLjN2q77Rg==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/abtesting": "1.6.1", - "@algolia/client-abtesting": "5.40.1", - "@algolia/client-analytics": "5.40.1", - "@algolia/client-common": "5.40.1", - "@algolia/client-insights": "5.40.1", - "@algolia/client-personalization": "5.40.1", - "@algolia/client-query-suggestions": "5.40.1", - "@algolia/client-search": "5.40.1", - "@algolia/ingestion": "1.40.1", - "@algolia/monitoring": "1.40.1", - "@algolia/recommend": "5.40.1", - "@algolia/requester-browser-xhr": "5.40.1", - "@algolia/requester-fetch": "5.40.1", - "@algolia/requester-node-http": "5.40.1" + "@algolia/abtesting": "1.14.1", + "@algolia/client-abtesting": "5.48.1", + "@algolia/client-analytics": "5.48.1", + "@algolia/client-common": "5.48.1", + "@algolia/client-insights": "5.48.1", + "@algolia/client-personalization": "5.48.1", + "@algolia/client-query-suggestions": "5.48.1", + "@algolia/client-search": "5.48.1", + "@algolia/ingestion": "1.48.1", + "@algolia/monitoring": "1.48.1", + "@algolia/recommend": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" }, "engines": { "node": ">= 14.0.0" @@ -5464,9 +5548,9 @@ } }, "node_modules/beasties": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.3.5.tgz", - "integrity": "sha512-NaWu+f4YrJxEttJSm16AzMIFtVldCvaJ68b1L098KpqXmxt9xOLtKoLkKxb8ekhOrLqEJAbvT6n6SEvB/sac7A==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.4.1.tgz", + "integrity": "sha512-2Imdcw3LznDuxAbJM26RHniOLAzE6WgrK8OuvVXCQtNBS8rsnD9zsSEa3fHl4hHpUY7BYTlrpvtPVbvu9G6neg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5477,10 +5561,11 @@ "htmlparser2": "^10.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.49", - "postcss-media-query-parser": "^0.2.3" + "postcss-media-query-parser": "^0.2.3", + "postcss-safe-parser": "^7.0.1" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, "node_modules/body-parser": { @@ -5535,27 +5620,26 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "fill-range": "^7.1.1" + "balanced-match": "^4.0.2" }, "engines": { - "node": ">=8" + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/browserslist": { @@ -5632,28 +5716,15 @@ } }, "node_modules/cacache/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, - "node_modules/cacache/node_modules/ssri": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.0.tgz", - "integrity": "sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -5714,23 +5785,6 @@ ], "license": "CC-BY-4.0" }, - "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, - "license": "MIT", - "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/chardet": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", @@ -5748,6 +5802,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, "license": "MIT", "dependencies": { "readdirp": "^4.0.1" @@ -5786,9 +5841,9 @@ } }, "node_modules/cli-spinners": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.3.0.tgz", - "integrity": "sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", "dev": true, "license": "MIT", "engines": { @@ -5980,9 +6035,9 @@ } }, "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==", + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "dev": true, "license": "MIT", "dependencies": { @@ -5991,6 +6046,10 @@ }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/cosmiconfig": { @@ -6139,9 +6198,9 @@ } }, "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -6353,9 +6412,9 @@ } }, "node_modules/esbuild": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.26.0.tgz", - "integrity": "sha512-3Hq7jri+tRrVWha+ZeIVhl4qJRha/XjRNSopvTsOaCvfPHrflTYTcUFcEjMKdxofsXXsdc4zjg5NOTnL4Gl57Q==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -6366,32 +6425,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.26.0", - "@esbuild/android-arm": "0.26.0", - "@esbuild/android-arm64": "0.26.0", - "@esbuild/android-x64": "0.26.0", - "@esbuild/darwin-arm64": "0.26.0", - "@esbuild/darwin-x64": "0.26.0", - "@esbuild/freebsd-arm64": "0.26.0", - "@esbuild/freebsd-x64": "0.26.0", - "@esbuild/linux-arm": "0.26.0", - "@esbuild/linux-arm64": "0.26.0", - "@esbuild/linux-ia32": "0.26.0", - "@esbuild/linux-loong64": "0.26.0", - "@esbuild/linux-mips64el": "0.26.0", - "@esbuild/linux-ppc64": "0.26.0", - "@esbuild/linux-riscv64": "0.26.0", - "@esbuild/linux-s390x": "0.26.0", - "@esbuild/linux-x64": "0.26.0", - "@esbuild/netbsd-arm64": "0.26.0", - "@esbuild/netbsd-x64": "0.26.0", - "@esbuild/openbsd-arm64": "0.26.0", - "@esbuild/openbsd-x64": "0.26.0", - "@esbuild/openharmony-arm64": "0.26.0", - "@esbuild/sunos-x64": "0.26.0", - "@esbuild/win32-arm64": "0.26.0", - "@esbuild/win32-ia32": "0.26.0", - "@esbuild/win32-x64": "0.26.0" + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" } }, "node_modules/escalade": { @@ -6424,33 +6483,30 @@ } }, "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.2.tgz", + "integrity": "sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", - "@eslint/plugin-kit": "^0.4.1", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.2", + "@eslint/config-helpers": "^0.5.2", + "@eslint/core": "^1.1.0", + "@eslint/plugin-kit": "^0.6.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", + "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", + "eslint-scope": "^9.1.1", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.1.1", + "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", @@ -6460,8 +6516,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^10.2.1", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -6469,7 +6524,7 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://eslint.org/donate" @@ -6484,12 +6539,14 @@ } }, "node_modules/eslint-scope": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.0.0.tgz", - "integrity": "sha512-+Yh0LeQKq+mW/tQArNj67tljR3L1HajDTQPuZOEwC00oBdoIDQrr89yBgjAlzAwRrY/5zDkM3v99iGHwz9y0dw==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.1.tgz", + "integrity": "sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, @@ -6514,9 +6571,9 @@ } }, "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -6530,42 +6587,14 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "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/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -6588,45 +6617,32 @@ "dev": true, "license": "MIT" }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.1.tgz", + "integrity": "sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" + "eslint-visitor-keys": "^5.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -6774,11 +6790,14 @@ } }, "node_modules/express-rate-limit": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.0.tgz", + "integrity": "sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==", "dev": true, "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, "engines": { "node": ">= 16" }, @@ -6879,20 +6898,6 @@ "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", "license": "MIT" }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -7081,18 +7086,18 @@ } }, "node_modules/glob": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", - "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "path-scurry": "^2.0.0" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -7118,35 +7123,6 @@ "dev": true, "license": "BSD-2-Clause" }, - "node_modules/glob/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -7204,12 +7180,11 @@ } }, "node_modules/hono": { - "version": "4.11.3", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.3.tgz", - "integrity": "sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w==", + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.5.tgz", + "integrity": "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -7228,9 +7203,9 @@ } }, "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -7245,9 +7220,9 @@ "license": "MIT" }, "node_modules/htmlparser2": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", - "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", @@ -7260,14 +7235,14 @@ "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", - "domutils": "^3.2.1", - "entities": "^6.0.0" + "domutils": "^3.2.2", + "entities": "^7.0.1" } }, "node_modules/htmlparser2/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -7334,9 +7309,9 @@ } }, "node_modules/iconv-lite": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", - "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "dev": true, "license": "MIT", "dependencies": { @@ -7373,26 +7348,10 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/ignore-walk/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/immutable": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", - "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", "dev": true, "license": "MIT" }, @@ -7430,13 +7389,13 @@ "license": "ISC" }, "node_modules/ini": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", - "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", "dev": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/ip-address": { @@ -7465,22 +7424,6 @@ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -7533,17 +7476,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -7653,9 +7585,9 @@ } }, "node_modules/jose": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", - "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.0.tgz", + "integrity": "sha512-xsfE1TcSCbUdo6U07tR0mvhg0flGxU8tPLbF03mirl2ukGQENhUg4ubGYQnhVH0b5stLlPM+WOqDkEl1R1y5sQ==", "dev": true, "license": "MIT", "funding": { @@ -7817,9 +7749,9 @@ } }, "node_modules/karma-coverage/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -7937,14 +7869,15 @@ } }, "node_modules/lmdb": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.4.3.tgz", - "integrity": "sha512-GWV1kVi6uhrXWqe+3NXWO73OYe8fto6q8JMo0HOpk1vf8nEyFWgo4CSNJpIFzsOxOrysVUlcO48qRbQfmKd1gA==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.5.1.tgz", + "integrity": "sha512-NYHA0MRPjvNX+vSw8Xxg6FLKxzAG+e7Pt8RqAQA/EehzHVXq9SxDqJIN3JL1hK0dweb884y8kIh6rkWvPyg9Wg==", "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, "dependencies": { + "@harperfast/extended-iterable": "^1.0.3", "msgpackr": "^1.11.2", "node-addon-api": "^6.1.0", "node-gyp-build-optional-packages": "5.2.2", @@ -7955,13 +7888,13 @@ "download-lmdb-prebuilds": "bin/download-prebuilds.js" }, "optionalDependencies": { - "@lmdb/lmdb-darwin-arm64": "3.4.3", - "@lmdb/lmdb-darwin-x64": "3.4.3", - "@lmdb/lmdb-linux-arm": "3.4.3", - "@lmdb/lmdb-linux-arm64": "3.4.3", - "@lmdb/lmdb-linux-x64": "3.4.3", - "@lmdb/lmdb-win32-arm64": "3.4.3", - "@lmdb/lmdb-win32-x64": "3.4.3" + "@lmdb/lmdb-darwin-arm64": "3.5.1", + "@lmdb/lmdb-darwin-x64": "3.5.1", + "@lmdb/lmdb-linux-arm": "3.5.1", + "@lmdb/lmdb-linux-arm64": "3.5.1", + "@lmdb/lmdb-linux-x64": "3.5.1", + "@lmdb/lmdb-win32-arm64": "3.5.1", + "@lmdb/lmdb-win32-x64": "3.5.1" } }, "node_modules/locate-path": { @@ -7981,9 +7914,9 @@ } }, "node_modules/lodash-es": { - "version": "4.17.22", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz", - "integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", "license": "MIT" }, "node_modules/lodash.clonedeep": { @@ -7999,13 +7932,6 @@ "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", "license": "MIT" }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, "node_modules/log-symbols": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", @@ -8111,9 +8037,9 @@ } }, "node_modules/magic-string": { - "version": "0.30.19", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", - "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8144,12 +8070,13 @@ "license": "ISC" }, "node_modules/make-fetch-happen": { - "version": "15.0.3", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.3.tgz", - "integrity": "sha512-iyyEpDty1mwW3dGlYXAJqC/azFn5PPvgKVwXayOGBSmKLxhKZ9fg4qIan2ePpp1vJIwfFiO34LAPZgq9SZW9Aw==", + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.4.tgz", + "integrity": "sha512-vM2sG+wbVeVGYcCm16mM3d5fuem9oC28n436HjsGO3LcxoTI8LNVa4rwZDn3f76+cWyT4GGJDxjTYU1I2nr6zw==", "dev": true, "license": "ISC", "dependencies": { + "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", @@ -8159,36 +8086,12 @@ "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", "ssri": "^13.0.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/make-fetch-happen/node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/make-fetch-happen/node_modules/ssri": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.0.tgz", - "integrity": "sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -8222,35 +8125,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -8292,27 +8166,27 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.2" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -8331,21 +8205,21 @@ } }, "node_modules/minipass-fetch": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.0.tgz", - "integrity": "sha512-fiCdUALipqgPWrOVTz9fw0XhcazULXOSU6ie40DDbX1F49p1dBrSRBuswndTx1x3vEb/g0FT7vC4c4C2u/mh3A==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz", + "integrity": "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==", "dev": true, "license": "MIT", "dependencies": { "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", + "minipass-sized": "^2.0.0", "minizlib": "^3.0.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" }, "optionalDependencies": { - "encoding": "^0.1.13" + "iconv-lite": "^0.7.2" } }, "node_modules/minipass-flush": { @@ -8415,38 +8289,18 @@ "license": "ISC" }, "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", + "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", "dev": true, "license": "ISC", "dependencies": { - "minipass": "^3.0.0" + "minipass": "^7.1.2" }, "engines": { "node": ">=8" } }, - "node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized/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, - "license": "ISC" - }, "node_modules/minizlib": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", @@ -8715,9 +8569,9 @@ } }, "node_modules/node-gyp": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.1.0.tgz", - "integrity": "sha512-W+RYA8jBnhSr2vrTtlPYPc1K+CSjGpVDRZxcqJcERZ8ND3A1ThWPHRwctTx3qC3oW99jt726jhdz3Y6ky87J4g==", + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.2.0.tgz", + "integrity": "sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8728,7 +8582,7 @@ "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", - "tar": "^7.5.2", + "tar": "^7.5.4", "tinyglobby": "^0.2.12", "which": "^6.0.0" }, @@ -8756,33 +8610,23 @@ } }, "node_modules/node-gyp/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=16" - } - }, - "node_modules/node-gyp/node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": ">=20" } }, "node_modules/node-gyp/node_modules/which": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", - "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", "dev": true, "license": "ISC", "dependencies": { - "isexe": "^3.1.1" + "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" @@ -8820,16 +8664,16 @@ "license": "MIT" }, "node_modules/npm-bundled": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-4.0.0.tgz", - "integrity": "sha512-IxaQZDMsqfQ2Lz37VvyyEtKLe8FsRZuysmedy/N06TU1RyVppYKXrO4xIhR0F+7ubIBox6Q7nir6fQI3ej39iA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz", + "integrity": "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==", "dev": true, "license": "ISC", "dependencies": { - "npm-normalize-package-bin": "^4.0.0" + "npm-normalize-package-bin": "^5.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm-install-checks": { @@ -8846,35 +8690,35 @@ } }, "node_modules/npm-normalize-package-bin": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", - "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz", + "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==", "dev": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm-package-arg": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.1.tgz", - "integrity": "sha512-6zqls5xFvJbgFjB1B2U6yITtyGBjDBORB7suI4zA4T/sZ1OmkMFlaQSNB/4K0LtXNA1t4OprAFxPisadK5O2ag==", + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.2.tgz", + "integrity": "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA==", "dev": true, "license": "ISC", "dependencies": { "hosted-git-info": "^9.0.0", - "proc-log": "^5.0.0", + "proc-log": "^6.0.0", "semver": "^7.3.5", - "validate-npm-package-name": "^6.0.0" + "validate-npm-package-name": "^7.0.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm-packlist": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.3.tgz", - "integrity": "sha512-zPukTwJMOu5X5uvm0fztwS5Zxyvmk38H/LfidkOMt3gbZVCyro2cD/ETzwzVPcWZA3JOyPznfUN/nkyFiyUbxg==", + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.4.tgz", + "integrity": "sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng==", "dev": true, "license": "ISC", "dependencies": { @@ -8885,16 +8729,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/npm-packlist/node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/npm-pick-manifest": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.3.tgz", @@ -8911,16 +8745,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/npm-pick-manifest/node_modules/npm-normalize-package-bin": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz", - "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/npm-registry-fetch": { "version": "19.1.1", "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.1.1.tgz", @@ -8941,16 +8765,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/npm-registry-fetch/node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -9055,9 +8869,9 @@ } }, "node_modules/ora": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-9.0.0.tgz", - "integrity": "sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.3.0.tgz", + "integrity": "sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw==", "dev": true, "license": "MIT", "dependencies": { @@ -9067,9 +8881,8 @@ "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", - "stdin-discarder": "^0.2.2", - "string-width": "^8.1.0", - "strip-ansi": "^7.1.2" + "stdin-discarder": "^0.3.1", + "string-width": "^8.1.0" }, "engines": { "node": ">=20" @@ -9145,16 +8958,16 @@ } }, "node_modules/pacote": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.0.3.tgz", - "integrity": "sha512-itdFlanxO0nmQv4ORsvA9K1wv40IPfB9OmWqfaJWvoJ30VKyHsqNgDVeG+TVhI7Gk7XW8slUy7cA9r6dF5qohw==", + "version": "21.3.1", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.3.1.tgz", + "integrity": "sha512-O0EDXi85LF4AzdjG74GUwEArhdvawi/YOHcsW6IijKNj7wm8IvEWNF5GnfuxNpQ/ZpO3L37+v8hqdVh8GgWYhg==", "dev": true, "license": "ISC", "dependencies": { "@npmcli/git": "^7.0.0", - "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/installed-package-contents": "^4.0.0", "@npmcli/package-json": "^7.0.0", - "@npmcli/promise-spawn": "^8.0.0", + "@npmcli/promise-spawn": "^9.0.0", "@npmcli/run-script": "^10.0.0", "cacache": "^20.0.0", "fs-minipass": "^3.0.0", @@ -9163,10 +8976,10 @@ "npm-packlist": "^10.0.1", "npm-pick-manifest": "^11.0.1", "npm-registry-fetch": "^19.0.0", - "proc-log": "^5.0.0", + "proc-log": "^6.0.0", "promise-retry": "^2.0.1", "sigstore": "^4.0.0", - "ssri": "^12.0.0", + "ssri": "^13.0.0", "tar": "^7.4.3" }, "bin": { @@ -9313,17 +9126,10 @@ "node": ">=8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, "node_modules/path-scurry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", - "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -9331,16 +9137,16 @@ "minipass": "^7.1.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -9386,9 +9192,9 @@ } }, "node_modules/piscina": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.1.3.tgz", - "integrity": "sha512-0u3N7H4+hbr40KjuVn2uNhOcthu/9usKhnw5vT3J7ply79v3D3M8naI00el9Klcy16x557VsEkkUQaHCWFXC/g==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.1.4.tgz", + "integrity": "sha512-7uU4ZnKeQq22t9AsmHGD2w4OYQGonwFnTypDypaWi7Qr2EvQIFVtG8J5D/3bE7W123Wdc9+v4CZDu5hJXVCtBg==", "dev": true, "license": "MIT", "engines": { @@ -9409,9 +9215,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "dev": true, "funding": [ { @@ -9444,6 +9250,33 @@ "dev": true, "license": "MIT" }, + "node_modules/postcss-safe-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -9455,13 +9288,13 @@ } }, "node_modules/proc-log": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", - "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/promise-retry": { @@ -9478,6 +9311,16 @@ "node": ">=10" } }, + "node_modules/promise-retry/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -9514,9 +9357,9 @@ } }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -9536,9 +9379,9 @@ "license": "MIT" }, "node_modules/quill": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz", - "integrity": "sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/quill/-/quill-2.0.2.tgz", + "integrity": "sha512-QfazNrhMakEdRG57IoYFwffUIr04LWJxbS/ZkidRFXYCQt63c1gK6Z7IHUXMx/Vh25WgPBU42oBaNzQ0K1R/xw==", "license": "BSD-3-Clause", "dependencies": { "eventemitter3": "^5.0.1", @@ -9594,6 +9437,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 14.18.0" @@ -9625,27 +9469,6 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "license": "MIT" }, - "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -9673,9 +9496,9 @@ } }, "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", "dev": true, "license": "MIT", "engines": { @@ -9690,14 +9513,14 @@ "license": "MIT" }, "node_modules/rolldown": { - "version": "1.0.0-beta.47", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.47.tgz", - "integrity": "sha512-Mid74GckX1OeFAOYz9KuXeWYhq3xkXbMziYIC+ULVdUzPTG9y70OBSBQDQn9hQP8u/AfhuYw1R0BSg15nBI4Dg==", + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.4.tgz", + "integrity": "sha512-V2tPDUrY3WSevrvU2E41ijZlpF+5PbZu4giH+VpNraaadsJGHa4fR6IFwsocVwEXDoAdIv5qgPPxgrvKAOIPtA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.96.0", - "@rolldown/pluginutils": "1.0.0-beta.47" + "@oxc-project/types": "=0.113.0", + "@rolldown/pluginutils": "1.0.0-rc.4" }, "bin": { "rolldown": "bin/cli.mjs" @@ -9706,26 +9529,25 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-beta.47", - "@rolldown/binding-darwin-arm64": "1.0.0-beta.47", - "@rolldown/binding-darwin-x64": "1.0.0-beta.47", - "@rolldown/binding-freebsd-x64": "1.0.0-beta.47", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.47", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.47", - "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.47", - "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.47", - "@rolldown/binding-linux-x64-musl": "1.0.0-beta.47", - "@rolldown/binding-openharmony-arm64": "1.0.0-beta.47", - "@rolldown/binding-wasm32-wasi": "1.0.0-beta.47", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.47", - "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.47", - "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.47" + "@rolldown/binding-android-arm64": "1.0.0-rc.4", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.4", + "@rolldown/binding-darwin-x64": "1.0.0-rc.4", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.4", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.4", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.4", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.4", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.4", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.4", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.4", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.4", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.4" } }, "node_modules/rollup": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", - "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", "dependencies": { @@ -9739,31 +9561,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.55.1", - "@rollup/rollup-android-arm64": "4.55.1", - "@rollup/rollup-darwin-arm64": "4.55.1", - "@rollup/rollup-darwin-x64": "4.55.1", - "@rollup/rollup-freebsd-arm64": "4.55.1", - "@rollup/rollup-freebsd-x64": "4.55.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", - "@rollup/rollup-linux-arm-musleabihf": "4.55.1", - "@rollup/rollup-linux-arm64-gnu": "4.55.1", - "@rollup/rollup-linux-arm64-musl": "4.55.1", - "@rollup/rollup-linux-loong64-gnu": "4.55.1", - "@rollup/rollup-linux-loong64-musl": "4.55.1", - "@rollup/rollup-linux-ppc64-gnu": "4.55.1", - "@rollup/rollup-linux-ppc64-musl": "4.55.1", - "@rollup/rollup-linux-riscv64-gnu": "4.55.1", - "@rollup/rollup-linux-riscv64-musl": "4.55.1", - "@rollup/rollup-linux-s390x-gnu": "4.55.1", - "@rollup/rollup-linux-x64-gnu": "4.55.1", - "@rollup/rollup-linux-x64-musl": "4.55.1", - "@rollup/rollup-openbsd-x64": "4.55.1", - "@rollup/rollup-openharmony-arm64": "4.55.1", - "@rollup/rollup-win32-arm64-msvc": "4.55.1", - "@rollup/rollup-win32-ia32-msvc": "4.55.1", - "@rollup/rollup-win32-x64-gnu": "4.55.1", - "@rollup/rollup-win32-x64-msvc": "4.55.1", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, @@ -9801,9 +9623,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.93.2", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz", - "integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==", + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", + "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", "dev": true, "license": "MIT", "dependencies": { @@ -9834,9 +9656,9 @@ } }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -10036,9 +9858,9 @@ } }, "node_modules/sirv": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", - "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", "dev": true, "license": "MIT", "dependencies": { @@ -10047,7 +9869,7 @@ "totalist": "^3.0.0" }, "engines": { - "node": ">= 10" + "node": ">=18" } }, "node_modules/slice-ansi": { @@ -10162,17 +9984,6 @@ "node": ">=0.10.0" } }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, "node_modules/spdx-exceptions": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", @@ -10181,9 +9992,9 @@ "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10192,23 +10003,23 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.22", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", - "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", "dev": true, "license": "CC0-1.0" }, "node_modules/ssri": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", - "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz", + "integrity": "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==", "dev": true, "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/statuses": { @@ -10222,9 +10033,9 @@ } }, "node_modules/stdin-discarder": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", - "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.1.tgz", + "integrity": "sha512-reExS1kSGoElkextOcPkel4NE99S0BWxjUHQeDFnR8S993JxpPX7KU4MNmO19NXhlJp+8dmdCbKQVNgLJh2teA==", "dev": true, "license": "MIT", "engines": { @@ -10292,23 +10103,10 @@ "node": ">=8" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/swiper": { - "version": "12.0.3", - "resolved": "https://registry.npmjs.org/swiper/-/swiper-12.0.3.tgz", - "integrity": "sha512-BHd6U1VPEIksrXlyXjMmRWO0onmdNPaTAFduzqR3pgjvi7KfmUCAm/0cj49u2D7B0zNjMw02TSeXfinC1hDCXg==", + "version": "12.1.2", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-12.1.2.tgz", + "integrity": "sha512-4gILrI3vXZqoZh71I1PALqukCFgk+gpOwe1tOvz5uE9kHtl2gTDzmYflYCwWvR4LOvCrJi6UEEU+gnuW5BtkgQ==", "funding": [ { "type": "patreon", @@ -10325,9 +10123,9 @@ } }, "node_modules/tar": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", - "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.10.tgz", + "integrity": "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -10367,20 +10165,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -10543,9 +10327,9 @@ } }, "node_modules/undici": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", - "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", + "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", "dev": true, "license": "MIT", "engines": { @@ -10553,9 +10337,9 @@ } }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "dev": true, "license": "MIT" }, @@ -10661,25 +10445,14 @@ "dev": true, "license": "MIT" }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, "node_modules/validate-npm-package-name": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.2.tgz", - "integrity": "sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", + "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", "dev": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/vary": { @@ -10693,13 +10466,13 @@ } }, "node_modules/vite": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", - "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", + "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -10767,494 +10540,10 @@ } } }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, "node_modules/watchpack": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", - "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "dev": true, "license": "MIT", "dependencies": { @@ -11280,9 +10569,9 @@ "license": "BSD-2-Clause" }, "node_modules/webpack-bundle-analyzer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-5.1.1.tgz", - "integrity": "sha512-UzoaIA0Aigo5lUvoUkIkSoHtUK5rBJh9e2vW3Eqct0jc/L8hcruBCz/jsXEvB1hDU1G3V94jo2EJqPcFKeSSeQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-5.2.0.tgz", + "integrity": "sha512-Etrauj1wYO/xjiz/Vfd6bW1lG9fEhrJpNmu10tv0X9kv+gyY3qiE09uYepqg1Xd0PxOvllRXwWYWjtQYoO/glQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11295,8 +10584,8 @@ "html-escaper": "^2.0.2", "opener": "^1.5.2", "picocolors": "^1.0.0", - "sirv": "^2.0.3", - "ws": "^7.3.1" + "sirv": "^3.0.2", + "ws": "^8.19.0" }, "bin": { "webpack-bundle-analyzer": "lib/bin/analyzer.js" @@ -11305,6 +10594,28 @@ "node": ">= 20.9.0" } }, + "node_modules/webpack-bundle-analyzer/node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "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/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -11547,9 +10858,9 @@ } }, "node_modules/zod": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", - "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", "funding": { @@ -11567,9 +10878,9 @@ } }, "node_modules/zone.js": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.16.0.tgz", - "integrity": "sha512-LqLPpIQANebrlxY6jKcYKdgN5DTXyyHAKnnWWjE5pPfEQ4n7j5zn7mOEEpwNZVKGqx3kKKmvplEmoBrvpgROTA==", + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.16.1.tgz", + "integrity": "sha512-dpvY17vxYIW3+bNrP0ClUlaiY0CiIRK3tnoLaGoQsQcY9/I/NpzIWQ7tQNhbV7LacQMpCII6wVzuL3tuWOyfuA==", "license": "MIT" }, "node_modules/zrender": { diff --git a/UI/Web/package.json b/UI/Web/package.json index 127544aeb..26c14768a 100644 --- a/UI/Web/package.json +++ b/UI/Web/package.json @@ -20,25 +20,25 @@ "private": true, "dependencies": { "@angular-slider/ngx-slider": "^21.0.0", - "@angular/animations": "^21.0.6", - "@angular/cdk": "^21.0.3", - "@angular/common": "^21.0.6", - "@angular/compiler": "^21.0.6", - "@angular/core": "^21.0.6", - "@angular/forms": "^21.0.6", - "@angular/localize": "^21.0.6", - "@angular/platform-browser": "^21.0.6", - "@angular/platform-browser-dynamic": "^21.0.6", - "@angular/router": "^21.0.6", - "@fortawesome/fontawesome-free": "^7.1.0", - "@iharbeck/ngx-virtual-scroller": "^19.0.1", + "@angular/animations": "^21.2.1", + "@angular/cdk": "^21.2.1", + "@angular/common": "^21.2.1", + "@angular/compiler": "^21.2.1", + "@angular/core": "^21.2.1", + "@angular/forms": "^21.2.1", + "@angular/localize": "^21.2.1", + "@angular/platform-browser": "^21.2.1", + "@angular/platform-browser-dynamic": "^21.2.1", + "@angular/router": "^21.2.1", + "@fortawesome/fontawesome-free": "^7.2.0", + "@iharbeck/ngx-virtual-scroller": "^20.0.0", "@iplab/ngx-color-picker": "^21.0.0", "@iplab/ngx-file-upload": "^21.0.0", - "@jsverse/transloco": "^8.2.0", - "@jsverse/transloco-locale": "^8.2.0", - "@jsverse/transloco-persist-lang": "^8.2.0", - "@jsverse/transloco-persist-translations": "^8.2.0", - "@jsverse/transloco-preload-langs": "^8.2.0", + "@jsverse/transloco": "^8.2.1", + "@jsverse/transloco-locale": "^8.2.1", + "@jsverse/transloco-persist-lang": "^8.2.1", + "@jsverse/transloco-persist-translations": "^8.2.1", + "@jsverse/transloco-preload-langs": "^8.2.1", "@microsoft/signalr": "^10.0.0", "@ng-bootstrap/ng-bootstrap": "^20.0.0", "@popperjs/core": "^2.11.7", @@ -60,34 +60,34 @@ "ngx-stars": "^1.6.5", "ngx-toastr": "^19.1.0", "nosleep.js": "^0.12.0", - "quill": "^2.0.3", + "quill": "^2.0.2", "rxjs": "^7.8.2", "screenfull": "^6.0.2", - "swiper": "^12.0.3", + "swiper": "^12.1.2", "tslib": "^2.8.1", - "zone.js": "^0.16.0" + "zone.js": "^0.16.1" }, "devDependencies": { - "@angular-eslint/builder": "^21.1.0", - "@angular-eslint/eslint-plugin": "^21.1.0", - "@angular-eslint/eslint-plugin-template": "^21.1.0", - "@angular-eslint/schematics": "^21.1.0", - "@angular-eslint/template-parser": "^21.1.0", - "@angular/build": "^21.0.3", - "@angular/cli": "^21.0.3", - "@angular/compiler-cli": "^21.0.6", + "@angular-eslint/builder": "^21.3.0", + "@angular-eslint/eslint-plugin": "^21.3.0", + "@angular-eslint/eslint-plugin-template": "^21.3.0", + "@angular-eslint/schematics": "^21.3.0", + "@angular-eslint/template-parser": "^21.3.0", + "@angular/build": "^21.2.1", + "@angular/cli": "^21.2.1", + "@angular/compiler-cli": "^21.2.1", "@types/d3": "^7.4.3", "@types/file-saver": "^2.0.7", "@types/luxon": "^3.7.1", "@types/marked": "^5.0.2", - "@types/node": "^25.0.2", - "@typescript-eslint/eslint-plugin": "^8.50.0", - "@typescript-eslint/parser": "^8.50.0", - "eslint": "^9.39.2", + "@types/node": "^25.3.3", + "@typescript-eslint/eslint-plugin": "^8.56.1", + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^10.0.2", "jsonminify": "^0.4.2", "karma-coverage": "~2.2.0", "ts-node": "~10.9.1", "typescript": "^5.9.3", - "webpack-bundle-analyzer": "^5.1.0" + "webpack-bundle-analyzer": "^5.2.0" } } diff --git a/UI/Web/src/app/_directives/echarts.directive.ts b/UI/Web/src/app/_directives/echarts.directive.ts index 3769c4a27..9dc1a14fd 100644 --- a/UI/Web/src/app/_directives/echarts.directive.ts +++ b/UI/Web/src/app/_directives/echarts.directive.ts @@ -10,19 +10,22 @@ import { OnInit, untracked } from '@angular/core'; -import {EChartsInitOpts, init, EChartsType, ComposeOption, registerTheme} from "echarts/core"; -import { BarSeriesOption, LineSeriesOption, PieSeriesOption } from 'echarts/charts'; +import {ComposeOption, EChartsInitOpts, EChartsType, init, registerTheme} from "echarts/core"; +import {BarSeriesOption, LineSeriesOption, PieSeriesOption} from 'echarts/charts'; import { - TitleComponentOption, - TooltipComponentOption, DatasetComponentOption, LegendComponentOption, + TitleComponentOption, ToolboxComponentOption, + TooltipComponentOption, } from 'echarts/components'; import {ThemeService} from "../_services/theme.service"; -import {asyncScheduler, Subject, Subscription, tap} from "rxjs"; +import {asyncScheduler, Subject, tap} from "rxjs"; import {throttleTime} from "rxjs/operators"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; +import {registerECharts} from "../../echarts"; + +registerECharts(); export type ECOption = ComposeOption< | BarSeriesOption diff --git a/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.html b/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.html index 78ff32a54..2243f1894 100644 --- a/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.html +++ b/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.html @@ -2,12 +2,12 @@
+ [imageUrl]="imageUrl()" />
- @if (entity.title | safeHtml; as info) { + @if (entity().title | safeHtml; as info) { @if (info !== '') {
@@ -20,7 +20,7 @@
- {{title}} + {{title()}}
diff --git a/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.ts b/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.ts index 0d9b9e123..2d4911a53 100644 --- a/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.ts +++ b/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.ts @@ -1,4 +1,4 @@ -import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core'; +import {ChangeDetectionStrategy, Component, computed, input} from '@angular/core'; import {ImageComponent} from "../../shared/image/image.component"; import {NextExpectedChapter} from "../../_models/series-detail/next-expected-chapter"; import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe"; @@ -12,25 +12,21 @@ import {translate, TranslocoDirective} from "@jsverse/transloco"; styleUrl: './next-expected-card.component.scss', changeDetection: ChangeDetectionStrategy.OnPush }) -export class NextExpectedCardComponent implements OnInit { - private readonly cdRef = inject(ChangeDetectorRef); - +export class NextExpectedCardComponent { + private readonly utcPipe = new UtcToLocalTimePipe(); /** * Card item url. Will internally handle error and missing covers */ - @Input() imageUrl = ''; + imageUrl = input.required(); /** * This is the entity we are representing. It will be returned if an action is executed. */ - @Input({required: true}) entity!: NextExpectedChapter; - title: string = ''; - - ngOnInit(): void { - if (this.entity.expectedDate) { - const utcPipe = new UtcToLocalTimePipe(); - this.title = translate('next-expected-card.title', {date: utcPipe.transform(this.entity.expectedDate, 'shortDate')}); + entity = input.required(); + title = computed(() => { + const expectedDate = this.entity()?.expectedDate; + if (expectedDate) { + return translate('next-expected-card.title', {date: this.utcPipe.transform(expectedDate, 'shortDate')}) } - this.cdRef.markForCheck(); - } - + return ''; + }); } diff --git a/UI/Web/src/app/cards/person-card/person-card.component.ts b/UI/Web/src/app/cards/person-card/person-card.component.ts index 848e0c3a8..b8c026aaf 100644 --- a/UI/Web/src/app/cards/person-card/person-card.component.ts +++ b/UI/Web/src/app/cards/person-card/person-card.component.ts @@ -3,7 +3,6 @@ import { ChangeDetectorRef, Component, contentChild, - DestroyRef, HostListener, inject, Input, @@ -12,7 +11,6 @@ import { } from '@angular/core'; import {ImageService} from "../../_services/image.service"; import {BulkSelectionService} from "../bulk-selection.service"; -import {MessageHubService} from "../../_services/message-hub.service"; import {ScrollService} from "../../_services/scroll.service"; import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap"; import {CardActionablesComponent} from "../../_single-module/card-actionables/card-actionables.component"; @@ -41,10 +39,8 @@ import {ActionItem} from "../../_models/actionables/action-item"; }) export class PersonCardComponent { - private readonly destroyRef = inject(DestroyRef); public readonly imageService = inject(ImageService); public readonly bulkSelectionService = inject(BulkSelectionService); - private readonly messageHub = inject(MessageHubService); private readonly scrollService = inject(ScrollService); private readonly cdRef = inject(ChangeDetectorRef); diff --git a/UI/Web/src/app/carousel/_components/carousel-reel/carousel-reel.component.ts b/UI/Web/src/app/carousel/_components/carousel-reel/carousel-reel.component.ts index 712a34987..a2116d6cb 100644 --- a/UI/Web/src/app/carousel/_components/carousel-reel/carousel-reel.component.ts +++ b/UI/Web/src/app/carousel/_components/carousel-reel/carousel-reel.component.ts @@ -3,14 +3,14 @@ import { ChangeDetectorRef, Component, computed, + contentChild, CUSTOM_ELEMENTS_SCHEMA, inject, input, Input, - signal, - TemplateRef, output, - contentChild + signal, + TemplateRef } from '@angular/core'; import {Swiper} from 'swiper/types'; import {register} from 'swiper/element/bundle'; diff --git a/UI/Web/src/app/ng-swipe/ng-swipe.directive.ts b/UI/Web/src/app/ng-swipe/ng-swipe.directive.ts index bd44ab5a1..5ab5d5ee2 100644 --- a/UI/Web/src/app/ng-swipe/ng-swipe.directive.ts +++ b/UI/Web/src/app/ng-swipe/ng-swipe.directive.ts @@ -1,4 +1,4 @@ -import {Directive, ElementRef, inject, Input, NgZone, OnDestroy, OnInit, output} from '@angular/core'; +import {Directive, ElementRef, inject, input, NgZone, OnDestroy, OnInit, output} from '@angular/core'; import {Subscription} from 'rxjs'; import {createSwipeSubscription, SwipeDirection, SwipeEvent} from './ag-swipe.core'; @@ -7,12 +7,10 @@ import {createSwipeSubscription, SwipeDirection, SwipeEvent} from './ag-swipe.co standalone: true }) export class SwipeDirective implements OnInit, OnDestroy { - private elementRef = inject(ElementRef); - private zone = inject(NgZone); + private readonly elementRef = inject(ElementRef); + private readonly zone = inject(NgZone); - private swipeSubscription: Subscription | undefined; - - @Input() restrictSwipeToLeftSide: boolean = false; + restrictSwipeToLeftSide = input(false); readonly swipeMove = output(); readonly swipeEnd = output(); readonly swipeLeft = output(); @@ -20,6 +18,8 @@ export class SwipeDirective implements OnInit, OnDestroy { readonly swipeUp = output(); readonly swipeDown = output(); + private swipeSubscription: Subscription | undefined; + ngOnInit() { this.zone.runOutsideAngular(() => { this.swipeSubscription = createSwipeSubscription({ @@ -36,7 +36,7 @@ export class SwipeDirective implements OnInit, OnDestroy { } private isSwipeWithinRestrictedArea(swipeEvent: SwipeEvent): boolean { - if (!this.restrictSwipeToLeftSide) return true; // If restriction is disabled, allow all swipes + if (!this.restrictSwipeToLeftSide()) return true; // If restriction is disabled, allow all swipes const elementRect = this.elementRef.nativeElement.getBoundingClientRect(); const touchAreaWidth = elementRect.width * 0.3; // Define the left area (30% of the element's width) diff --git a/UI/Web/src/main.ts b/UI/Web/src/main.ts index 7c9e9533d..bfb178099 100644 --- a/UI/Web/src/main.ts +++ b/UI/Web/src/main.ts @@ -22,8 +22,6 @@ import {getSaver, SAVER} from "./app/_providers/saver.provider"; import {APP_BASE_HREF, PlatformLocation} from "@angular/common"; import {provideTranslocoPersistTranslations} from '@jsverse/transloco-persist-translations'; import {HttpLoader} from "./httpLoader"; -import {register as registerSwiperElements} from 'swiper/element/bundle'; -import {ColorPickerModule} from "@iplab/ngx-color-picker"; import {clientInfoInterceptor} from "./app/_interceptors/client-info.interceptor"; import { PreloadAllModules, @@ -36,7 +34,6 @@ import { } from "@angular/router"; import {KavitaTitleStrategy} from "./app/_services/kavita-title.strategy"; import {routingErrorHandler} from "./app/_interceptors/routing-error.handler"; -import {registerECharts} from "./echarts"; import {NgbModalConfig, NgbRatingConfig} from "@ng-bootstrap/ng-bootstrap"; import {DefaultModalOptions} from "./app/_models/modal/modal-options"; import {ToastNoAnimationModule} from "ngx-toastr"; @@ -49,8 +46,6 @@ if (disableAnimations) { document.documentElement.classList.add('no-animations'); } -registerSwiperElements(); -registerECharts(); function transformLanguageCodes(arr: Array) { const transformedArray: Array = []; @@ -176,8 +171,7 @@ bootstrapApplication(AppComponent, { countDuplicates: true, autoDismiss: true }), - NgCircleProgressModule.forRoot(), - ColorPickerModule, + NgCircleProgressModule.forRoot() ), provideRouter(routes, withComponentInputBinding(), diff --git a/build.sh b/build.sh index e34754794..59346154a 100755 --- a/build.sh +++ b/build.sh @@ -50,15 +50,15 @@ BuildUI() { ProgressStart 'Building UI' echo 'Removing old wwwroot' - rm -rf API/wwwroot/* + rm -rf Kavita.Server/wwwroot/* cd UI/Web/ || exit echo 'Installing web dependencies' npm install --legacy-peer-deps echo 'Building UI' npm run prod echo 'Copying back to Kavita wwwroot' - mkdir -p ../../API/wwwroot - cp -R dist/browser/* ../../API/wwwroot + mkdir -p ../../Kavita.Server/wwwroot + cp -R dist/browser/* ../../Kavita.Server/wwwroot cd ../../ || exit ProgressEnd 'Building UI' } @@ -72,7 +72,7 @@ Package() # TODO: Use no-restore? Because Build should have already done it for us echo "Building" - cd API + cd Kavita.Server echo dotnet publish -c Release --self-contained --runtime $runtime -o "$lOutputFolder" dotnet publish -c Release --self-contained --runtime $runtime -o "$lOutputFolder" @@ -92,20 +92,17 @@ Package() echo "Copying LICENSE" cp ../LICENSE "$lOutputFolder"/LICENSE.txt - echo "Renaming API -> Kavita" + echo "Renaming Kavita.Server -> Kavita" if [ $runtime == "win-x64" ] || [ $runtime == "win-x86" ] then - mv "$lOutputFolder"/API.exe "$lOutputFolder"/Kavita.exe + mv "$lOutputFolder"/Kavita.Server.exe "$lOutputFolder"/Kavita.exe else - mv "$lOutputFolder"/API "$lOutputFolder"/Kavita + mv "$lOutputFolder"/Kavita.Server "$lOutputFolder"/Kavita fi + mkdir -p $lOutputFolder/config echo "Copying appsettings.json" cp config/appsettings.json $lOutputFolder/config/appsettings-init.json - echo "Removing appsettings.Development.json" - rm $lOutputFolder/config/appsettings.Development.json - echo "Removing appsettings.json" - rm $lOutputFolder/config/appsettings.json echo "Creating tar" cd ../$outputFolder/"$runtime"/ diff --git a/docker-build.sh b/docker-build.sh index 072676a78..0659144fe 100755 --- a/docker-build.sh +++ b/docker-build.sh @@ -32,15 +32,15 @@ BuildUI() { ProgressStart 'Building UI' echo 'Removing old wwwroot' - rm -rf API/wwwroot/* + rm -rf Kavita.Server/wwwroot/* cd UI/Web/ || exit echo 'Installing web dependencies' npm install --legacy-peer-deps echo 'Building UI' npm run prod echo 'Copying back to Kavita wwwroot' - mkdir -p ../../API/wwwroot - cp -R dist/browser/* ../../API/wwwroot + mkdir -p ../../Kavita.Server/wwwroot + cp -R dist/browser/* ../../Kavita.Server/wwwroot cd ../../ || exit ProgressEnd 'Building UI' } @@ -54,7 +54,7 @@ Package() # TODO: Use no-restore? Because Build should have already done it for us echo "Building" - cd API + cd Kavita.Server echo dotnet publish -c Release --no-restore --self-contained --runtime $runtime -o "$lOutputFolder" dotnet publish -c Release --no-restore --self-contained --runtime $runtime -o "$lOutputFolder" @@ -64,8 +64,8 @@ Package() echo "Copying LICENSE" cp ../LICENSE "$lOutputFolder"/LICENSE.txt - echo "Renaming API -> Kavita" - mv "$lOutputFolder"/API "$lOutputFolder"/Kavita + echo "Renaming Kavita.Server -> Kavita" + mv "$lOutputFolder"/Kavita.Server "$lOutputFolder"/Kavita echo "Creating tar" cd ../$outputFolder/"$runtime"/