diff --git a/.github/workflows/sonar-scan.yml b/.github/workflows/sonar-scan.yml index 31f60b8e7..c58c177b1 100644 --- a/.github/workflows/sonar-scan.yml +++ b/.github/workflows/sonar-scan.yml @@ -20,11 +20,11 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v2 with: - dotnet-version: 6.0.x + dotnet-version: 7.0.x - name: Install Swashbuckle CLI shell: powershell - run: dotnet tool install -g --version 6.4.0 Swashbuckle.AspNetCore.Cli + run: dotnet tool install -g --version 6.5.0 Swashbuckle.AspNetCore.Cli - name: Install dependencies run: dotnet restore @@ -88,10 +88,10 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v2 with: - dotnet-version: 6.0.x + dotnet-version: 7.0.x - name: Install Swashbuckle CLI - run: dotnet tool install -g --version 6.4.0 Swashbuckle.AspNetCore.Cli + run: dotnet tool install -g --version 6.5.0 Swashbuckle.AspNetCore.Cli - name: Install dependencies run: dotnet restore @@ -178,10 +178,10 @@ jobs: - name: Compile dotnet app uses: actions/setup-dotnet@v2 with: - dotnet-version: 6.0.x + dotnet-version: 7.0.x - name: Install Swashbuckle CLI - run: dotnet tool install -g --version 6.4.0 Swashbuckle.AspNetCore.Cli + run: dotnet tool install -g --version 6.5.0 Swashbuckle.AspNetCore.Cli - run: ./monorepo-build.sh @@ -295,9 +295,9 @@ jobs: - name: Compile dotnet app uses: actions/setup-dotnet@v2 with: - dotnet-version: 6.0.x + dotnet-version: 7.0.x - name: Install Swashbuckle CLI - run: dotnet tool install -g --version 6.4.0 Swashbuckle.AspNetCore.Cli + run: dotnet tool install -g --version 6.5.0 Swashbuckle.AspNetCore.Cli - run: ./monorepo-build.sh diff --git a/API.Benchmark/API.Benchmark.csproj b/API.Benchmark/API.Benchmark.csproj index 11ef151a2..e6dc8301a 100644 --- a/API.Benchmark/API.Benchmark.csproj +++ b/API.Benchmark/API.Benchmark.csproj @@ -1,7 +1,7 @@ - net6.0 + net7.0 Exe @@ -10,9 +10,9 @@ - - - + + + diff --git a/API.Benchmark/ArchiveServiceBenchmark.cs b/API.Benchmark/ArchiveServiceBenchmark.cs index e24589205..e856aa7c8 100644 --- a/API.Benchmark/ArchiveServiceBenchmark.cs +++ b/API.Benchmark/ArchiveServiceBenchmark.cs @@ -16,7 +16,7 @@ namespace API.Benchmark; [MemoryDiagnoser] [RankColumn] [Orderer(SummaryOrderPolicy.FastestToSlowest)] -[SimpleJob(launchCount: 1, warmupCount: 5, targetCount: 20)] +[SimpleJob(launchCount: 1, warmupCount: 5, invocationCount: 20)] public class ArchiveServiceBenchmark { private readonly ArchiveService _archiveService; diff --git a/API.Benchmark/CleanTitleBenchmark.cs b/API.Benchmark/CleanTitleBenchmark.cs index 90310a9ef..c3a383647 100644 --- a/API.Benchmark/CleanTitleBenchmark.cs +++ b/API.Benchmark/CleanTitleBenchmark.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; -using System.Text.RegularExpressions; using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Order; namespace API.Benchmark; diff --git a/API.Benchmark/EpubBenchmark.cs b/API.Benchmark/EpubBenchmark.cs index 1df4f176e..3dd5e36c1 100644 --- a/API.Benchmark/EpubBenchmark.cs +++ b/API.Benchmark/EpubBenchmark.cs @@ -14,23 +14,12 @@ namespace API.Benchmark; [MemoryDiagnoser] [RankColumn] [Orderer(SummaryOrderPolicy.FastestToSlowest)] -[SimpleJob(launchCount: 1, warmupCount: 5, targetCount: 20)] +[SimpleJob(launchCount: 1, warmupCount: 5, invocationCount: 20)] public class EpubBenchmark { private const string FilePath = @"E:\Books\Invaders of the Rokujouma\Invaders of the Rokujouma - Volume 01.epub"; private readonly Regex WordRegex = new Regex(@"\b\w+\b", RegexOptions.Compiled | RegexOptions.IgnoreCase); - // [Benchmark] - // public async Task GetWordCount_PassByString() - // { - // using var book = await EpubReader.OpenBookAsync(FilePath, BookService.BookReaderOptions); - // foreach (var bookFile in book.Content.Html.Values) - // { - // GetBookWordCount_PassByString(await bookFile.ReadContentAsTextAsync()); - // ; - // } - // } - [Benchmark] public async Task GetWordCount_PassByRef() { diff --git a/API.Benchmark/ParserBenchmarks.cs b/API.Benchmark/ParserBenchmarks.cs index d7706a3f4..0dabc560b 100644 --- a/API.Benchmark/ParserBenchmarks.cs +++ b/API.Benchmark/ParserBenchmarks.cs @@ -74,5 +74,24 @@ public class ParserBenchmarks } } + [Benchmark] + public void Test_CharacterReplace() + { + foreach (var name in _names) + { + var d = name.Contains('a'); + } + } + + [Benchmark] + public void Test_StringReplace() + { + foreach (var name in _names) + { + + var d = name.Contains("a"); + } + } + } diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj index cda56af18..227d4c4df 100644 --- a/API.Tests/API.Tests.csproj +++ b/API.Tests/API.Tests.csproj @@ -1,39 +1,38 @@ - net6.0 - + net7.0 false - - + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + - - + + - + diff --git a/API.Tests/AbstractDbTest.cs b/API.Tests/AbstractDbTest.cs index 1a9926345..79056b500 100644 --- a/API.Tests/AbstractDbTest.cs +++ b/API.Tests/AbstractDbTest.cs @@ -29,6 +29,7 @@ public abstract class AbstractDbTest protected const string BackupDirectory = "C:/kavita/config/backups/"; protected const string LogDirectory = "C:/kavita/config/logs/"; protected const string BookmarkDirectory = "C:/kavita/config/bookmarks/"; + protected const string SiteThemeDirectory = "C:/kavita/config/themes/"; protected const string TempDirectory = "C:/kavita/config/temp/"; protected const string DataDirectory = "C:/data/"; @@ -103,6 +104,7 @@ public abstract class AbstractDbTest fileSystem.AddDirectory(CoverImageDirectory); fileSystem.AddDirectory(BackupDirectory); fileSystem.AddDirectory(BookmarkDirectory); + fileSystem.AddDirectory(SiteThemeDirectory); fileSystem.AddDirectory(LogDirectory); fileSystem.AddDirectory(TempDirectory); fileSystem.AddDirectory(DataDirectory); diff --git a/API.Tests/Entities/SeriesTest.cs b/API.Tests/Entities/SeriesTest.cs index 0b49bd3dd..72552fb0b 100644 --- a/API.Tests/Entities/SeriesTest.cs +++ b/API.Tests/Entities/SeriesTest.cs @@ -1,4 +1,5 @@ using API.Data; +using API.Extensions; using Xunit; namespace API.Tests.Entities; @@ -12,7 +13,7 @@ public class SeriesTest [InlineData("Darker than Black")] public void CreateSeries(string name) { - var key = API.Services.Tasks.Scanner.Parser.Parser.Normalize(name); + var key = name.ToNormalized(); var series = DbFactory.Series(name); Assert.Equal(0, series.Id); Assert.Equal(0, series.Pages); diff --git a/API.Tests/Extensions/ParserInfoListExtensionsTests.cs b/API.Tests/Extensions/ParserInfoListExtensionsTests.cs index b6a5ca362..d0247a089 100644 --- a/API.Tests/Extensions/ParserInfoListExtensionsTests.cs +++ b/API.Tests/Extensions/ParserInfoListExtensionsTests.cs @@ -27,7 +27,7 @@ public class ParserInfoListExtensions [InlineData(new[] {"1", "1", "3-5", "5", "8", "0", "0"}, new[] {"1", "3-5", "5", "8", "0"})] public void DistinctVolumesTest(string[] volumeNumbers, string[] expectedNumbers) { - var infos = volumeNumbers.Select(n => new ParserInfo() {Volumes = n}).ToList(); + var infos = volumeNumbers.Select(n => new ParserInfo() {Series = "", Volumes = n}).ToList(); Assert.Equal(expectedNumbers, infos.DistinctVolumes()); } diff --git a/API.Tests/Extensions/QueryableExtensionsTests.cs b/API.Tests/Extensions/QueryableExtensionsTests.cs index ee1ada416..76e8752ac 100644 --- a/API.Tests/Extensions/QueryableExtensionsTests.cs +++ b/API.Tests/Extensions/QueryableExtensionsTests.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; using System.Linq; +using API.Data; using API.Data.Misc; using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; using API.Extensions; +using API.Tests.Helpers.Builders; using Xunit; namespace API.Tests.Extensions; @@ -18,27 +20,24 @@ public class QueryableExtensionsTests { var items = new List() { - new Series() - { - Metadata = new SeriesMetadata() + new SeriesBuilder("Test 1") + .WithMetadata( new SeriesMetadata() { AgeRating = AgeRating.Teen, - } - }, - new Series() - { - Metadata = new SeriesMetadata() + }) + .Build(), + new SeriesBuilder("Test 2") + .WithMetadata( new SeriesMetadata() { AgeRating = AgeRating.Unknown, - } - }, - new Series() - { - Metadata = new SeriesMetadata() + }) + .Build(), + new SeriesBuilder("Test 3") + .WithMetadata( new SeriesMetadata() { AgeRating = AgeRating.X18Plus, - } - }, + }) + .Build() }; var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction() @@ -58,6 +57,8 @@ public class QueryableExtensionsTests { new CollectionTag() { + Title = "Test", + NormalizedTitle = "Test".ToNormalized(), SeriesMetadatas = new List() { new SeriesMetadata() @@ -68,6 +69,8 @@ public class QueryableExtensionsTests }, new CollectionTag() { + Title = "Test", + NormalizedTitle = "Test".ToNormalized(), SeriesMetadatas = new List() { new SeriesMetadata() @@ -82,6 +85,8 @@ public class QueryableExtensionsTests }, new CollectionTag() { + Title = "Test", + NormalizedTitle = "Test".ToNormalized(), SeriesMetadatas = new List() { new SeriesMetadata() @@ -109,6 +114,8 @@ public class QueryableExtensionsTests { new Genre() { + Title = "A", + NormalizedTitle = "A".ToNormalized(), SeriesMetadatas = new List() { new SeriesMetadata() @@ -119,6 +126,8 @@ public class QueryableExtensionsTests }, new Genre() { + Title = "B", + NormalizedTitle = "B".ToNormalized(), SeriesMetadatas = new List() { new SeriesMetadata() @@ -133,6 +142,8 @@ public class QueryableExtensionsTests }, new Genre() { + Title = "C", + NormalizedTitle = "C".ToNormalized(), SeriesMetadatas = new List() { new SeriesMetadata() @@ -160,6 +171,8 @@ public class QueryableExtensionsTests { new Tag() { + Title = "Test 1", + NormalizedTitle = "Test 1".ToNormalized(), SeriesMetadatas = new List() { new SeriesMetadata() @@ -170,6 +183,8 @@ public class QueryableExtensionsTests }, new Tag() { + Title = "Test 2", + NormalizedTitle = "Test 2".ToNormalized(), SeriesMetadatas = new List() { new SeriesMetadata() @@ -184,6 +199,8 @@ public class QueryableExtensionsTests }, new Tag() { + Title = "Test 3", + NormalizedTitle = "Test 3".ToNormalized(), SeriesMetadatas = new List() { new SeriesMetadata() @@ -258,20 +275,12 @@ public class QueryableExtensionsTests [InlineData(false, 1)] public void RestrictAgainstAgeRestriction_ReadingList_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount) { + var items = new List() { - new ReadingList() - { - AgeRating = AgeRating.Teen, - }, - new ReadingList() - { - AgeRating = AgeRating.Unknown, - }, - new ReadingList() - { - AgeRating = AgeRating.X18Plus - }, + DbFactory.ReadingList("Test List", null, false, AgeRating.Teen), + DbFactory.ReadingList("Test List", null, false, AgeRating.Unknown), + DbFactory.ReadingList("Test List", null, false, AgeRating.X18Plus), }; var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(new AgeRestriction() diff --git a/API.Tests/Extensions/SeriesExtensionsTests.cs b/API.Tests/Extensions/SeriesExtensionsTests.cs index f8dce8876..99d0825a0 100644 --- a/API.Tests/Extensions/SeriesExtensionsTests.cs +++ b/API.Tests/Extensions/SeriesExtensionsTests.cs @@ -3,124 +3,31 @@ using System.Linq; using API.Comparators; using API.Entities; using API.Entities.Enums; -using API.Entities.Metadata; using API.Extensions; -using API.Parser; -using API.Services.Tasks.Scanner; +using API.Tests.Helpers.Builders; using Xunit; namespace API.Tests.Extensions; public class SeriesExtensionsTests { - [Theory] - [InlineData(new [] {"Darker than Black", "Darker Than Black", "Darker than Black"}, new [] {"Darker than Black"}, true)] - [InlineData(new [] {"Darker than Black", "Darker Than Black", "Darker than Black"}, new [] {"Darker_than_Black"}, true)] - [InlineData(new [] {"Darker than Black", "Darker Than Black", "Darker than Black"}, new [] {"Darker then Black!"}, false)] - [InlineData(new [] {"Salem's Lot", "Salem's Lot", "Salem's Lot"}, new [] {"Salem's Lot"}, true)] - [InlineData(new [] {"Salem's Lot", "Salem's Lot", "Salem's Lot"}, new [] {"salems lot"}, true)] - [InlineData(new [] {"Salem's Lot", "Salem's Lot", "Salem's Lot"}, new [] {"salem's lot"}, true)] - // Different normalizations pass as we check normalization against an on-the-fly calculation so we don't delete series just because we change how normalization works - [InlineData(new [] {"Salem's Lot", "Salem's Lot", "Salem's Lot", "salems lot"}, new [] {"salem's lot"}, true)] - [InlineData(new [] {"Rent-a-Girlfriend", "Rent-a-Girlfriend", "Kanojo, Okarishimasu", "rentagirlfriend"}, new [] {"Kanojo, Okarishimasu"}, true)] - public void NameInListTest(string[] seriesInput, string[] list, bool expected) - { - var series = new Series() - { - Name = seriesInput[0], - LocalizedName = seriesInput[1], - OriginalName = seriesInput[2], - NormalizedName = seriesInput.Length == 4 ? seriesInput[3] : API.Services.Tasks.Scanner.Parser.Parser.Normalize(seriesInput[0]), - Metadata = new SeriesMetadata() - }; - - Assert.Equal(expected, series.NameInList(list)); - } - - [Theory] - [InlineData(new [] {"Darker than Black", "Darker Than Black", "Darker than Black"}, new [] {"Darker than Black"}, MangaFormat.Archive, true)] - [InlineData(new [] {"Darker than Black", "Darker Than Black", "Darker than Black"}, new [] {"Darker_than_Black"}, MangaFormat.Archive, true)] - [InlineData(new [] {"Darker than Black", "Darker Than Black", "Darker than Black"}, new [] {"Darker then Black!"}, MangaFormat.Archive, false)] - [InlineData(new [] {"Salem's Lot", "Salem's Lot", "Salem's Lot"}, new [] {"Salem's Lot"}, MangaFormat.Archive, true)] - [InlineData(new [] {"Salem's Lot", "Salem's Lot", "Salem's Lot"}, new [] {"salems lot"}, MangaFormat.Archive, true)] - [InlineData(new [] {"Salem's Lot", "Salem's Lot", "Salem's Lot"}, new [] {"salem's lot"}, MangaFormat.Archive, true)] - // Different normalizations pass as we check normalization against an on-the-fly calculation so we don't delete series just because we change how normalization works - [InlineData(new [] {"Salem's Lot", "Salem's Lot", "Salem's Lot", "salems lot"}, new [] {"salem's lot"}, MangaFormat.Archive, true)] - [InlineData(new [] {"Rent-a-Girlfriend", "Rent-a-Girlfriend", "Kanojo, Okarishimasu", "rentagirlfriend"}, new [] {"Kanojo, Okarishimasu"}, MangaFormat.Archive, true)] - public void NameInListParserInfoTest(string[] seriesInput, string[] list, MangaFormat format, bool expected) - { - var series = new Series() - { - Name = seriesInput[0], - LocalizedName = seriesInput[1], - OriginalName = seriesInput[2], - NormalizedName = seriesInput.Length == 4 ? seriesInput[3] : API.Services.Tasks.Scanner.Parser.Parser.Normalize(seriesInput[0]), - Metadata = new SeriesMetadata(), - }; - - var parserInfos = list.Select(s => new ParsedSeries() - { - Name = s, - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize(s), - }).ToList(); - - // This doesn't do any checks against format - Assert.Equal(expected, series.NameInList(parserInfos)); - } - - - [Theory] - [InlineData(new [] {"Darker than Black", "Darker Than Black", "Darker than Black"}, "Darker than Black", true)] - [InlineData(new [] {"Rent-a-Girlfriend", "Rent-a-Girlfriend", "Kanojo, Okarishimasu", "rentagirlfriend"}, "Kanojo, Okarishimasu", true)] - [InlineData(new [] {"Rent-a-Girlfriend", "Rent-a-Girlfriend", "Kanojo, Okarishimasu", "rentagirlfriend"}, "Rent", false)] - public void NameInParserInfoTest(string[] seriesInput, string parserSeries, bool expected) - { - var series = new Series() - { - Name = seriesInput[0], - LocalizedName = seriesInput[1], - OriginalName = seriesInput[2], - NormalizedName = seriesInput.Length == 4 ? seriesInput[3] : API.Services.Tasks.Scanner.Parser.Parser.Normalize(seriesInput[0]), - Metadata = new SeriesMetadata() - }; - var info = new ParserInfo - { - Series = parserSeries - }; - - Assert.Equal(expected, series.NameInParserInfo(info)); - } - [Fact] public void GetCoverImage_MultipleSpecials_Comics() { - var series = new Series() - { - Format = MangaFormat.Archive, - Volumes = new List() - { - new Volume() - { - Number = 0, - Name = API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume, - Chapters = new List() - { - new Chapter() - { - IsSpecial = true, - Number = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, - CoverImage = "Special 1", - }, - new Chapter() - { - IsSpecial = true, - Number = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, - CoverImage = "Special 2", - } - }, - } - } - }; + var series = new SeriesBuilder("Test 1") + .WithFormat(MangaFormat.Archive) + .WithVolume(new VolumeBuilder("0") + .WithName(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume) + .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithCoverImage("Special 1") + .WithIsSpecial(true) + .Build()) + .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithCoverImage("Special 2") + .WithIsSpecial(true) + .Build()) + .Build()) + .Build(); Assert.Equal("Special 1", series.GetCoverImage()); @@ -129,33 +36,20 @@ public class SeriesExtensionsTests [Fact] public void GetCoverImage_MultipleSpecials_Books() { - var series = new Series() - { - Format = MangaFormat.Epub, - Volumes = new List() - { - new Volume() - { - Number = 0, - Name = API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume, - Chapters = new List() - { - new Chapter() - { - IsSpecial = true, - Number = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, - CoverImage = "Special 1", - }, - new Chapter() - { - IsSpecial = true, - Number = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, - CoverImage = "Special 2", - } - }, - } - } - }; + var series = new SeriesBuilder("Test 1") + .WithFormat(MangaFormat.Archive) + .WithVolume(new VolumeBuilder("0") + .WithName(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume) + .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithCoverImage("Special 1") + .WithIsSpecial(true) + .Build()) + .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) + .WithCoverImage("Special 2") + .WithIsSpecial(true) + .Build()) + .Build()) + .Build(); Assert.Equal("Special 1", series.GetCoverImage()); } @@ -163,33 +57,20 @@ public class SeriesExtensionsTests [Fact] public void GetCoverImage_JustChapters_Comics() { - var series = new Series() - { - Format = MangaFormat.Archive, - Volumes = new List() - { - new Volume() - { - Number = 0, - Name = API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume, - Chapters = new List() - { - new Chapter() - { - IsSpecial = false, - Number = "2.5", - CoverImage = "Special 1", - }, - new Chapter() - { - IsSpecial = false, - Number = "2", - CoverImage = "Special 2", - } - }, - } - } - }; + var series = new SeriesBuilder("Test 1") + .WithFormat(MangaFormat.Archive) + .WithVolume(new VolumeBuilder("0") + .WithName(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume) + .WithChapter(new ChapterBuilder("2.5") + .WithIsSpecial(false) + .WithCoverImage("Special 1") + .Build()) + .WithChapter(new ChapterBuilder("2") + .WithIsSpecial(false) + .WithCoverImage("Special 2") + .Build()) + .Build()) + .Build(); foreach (var vol in series.Volumes) { @@ -202,39 +83,24 @@ public class SeriesExtensionsTests [Fact] public void GetCoverImage_JustChaptersAndSpecials_Comics() { - var series = new Series() - { - Format = MangaFormat.Archive, - Volumes = new List() - { - new Volume() - { - Number = 0, - Name = API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume, - Chapters = new List() - { - new Chapter() - { - IsSpecial = false, - Number = "2.5", - CoverImage = "Special 1", - }, - new Chapter() - { - IsSpecial = false, - Number = "2", - CoverImage = "Special 2", - }, - new Chapter() - { - IsSpecial = true, - Number = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, - CoverImage = "Special 3", - } - }, - } - } - }; + var series = new SeriesBuilder("Test 1") + .WithFormat(MangaFormat.Archive) + .WithVolume(new VolumeBuilder("0") + .WithName(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume) + .WithChapter(new ChapterBuilder("2.5") + .WithIsSpecial(false) + .WithCoverImage("Special 1") + .Build()) + .WithChapter(new ChapterBuilder("2") + .WithIsSpecial(false) + .WithCoverImage("Special 2") + .Build()) + .WithChapter(new ChapterBuilder("0") + .WithIsSpecial(true) + .WithCoverImage("Special 3") + .Build()) + .Build()) + .Build(); foreach (var vol in series.Volumes) { @@ -247,54 +113,31 @@ public class SeriesExtensionsTests [Fact] public void GetCoverImage_VolumesChapters_Comics() { - var series = new Series() - { - Format = MangaFormat.Archive, - Volumes = new List() - { - new Volume() - { - Number = 0, - Name = API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume, - Chapters = new List() - { - new Chapter() - { - IsSpecial = false, - Number = "2.5", - CoverImage = "Special 1", - }, - new Chapter() - { - IsSpecial = false, - Number = "2", - CoverImage = "Special 2", - }, - new Chapter() - { - IsSpecial = true, - Number = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, - CoverImage = "Special 3", - } - }, - }, - new Volume() - { - Number = 1, - Name = "1", - Chapters = new List() - { - new Chapter() - { - IsSpecial = false, - Number = "0", - CoverImage = "Volume 1", - }, - - }, - } - } - }; + var series = new SeriesBuilder("Test 1") + .WithFormat(MangaFormat.Archive) + .WithVolume(new VolumeBuilder("0") + .WithName(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume) + .WithChapter(new ChapterBuilder("2.5") + .WithIsSpecial(false) + .WithCoverImage("Special 1") + .Build()) + .WithChapter(new ChapterBuilder("2") + .WithIsSpecial(false) + .WithCoverImage("Special 2") + .Build()) + .WithChapter(new ChapterBuilder("0") + .WithIsSpecial(true) + .WithCoverImage("Special 3") + .Build()) + .Build()) + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("0") + .WithIsSpecial(false) + .WithCoverImage("Volume 1") + .Build()) + .Build()) + .Build(); foreach (var vol in series.Volumes) { @@ -307,54 +150,31 @@ public class SeriesExtensionsTests [Fact] public void GetCoverImage_VolumesChaptersAndSpecials_Comics() { - var series = new Series() - { - Format = MangaFormat.Archive, - Volumes = new List() - { - new Volume() - { - Number = 0, - Name = API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume, - Chapters = new List() - { - new Chapter() - { - IsSpecial = false, - Number = "2.5", - CoverImage = "Special 1", - }, - new Chapter() - { - IsSpecial = false, - Number = "2", - CoverImage = "Special 2", - }, - new Chapter() - { - IsSpecial = true, - Number = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, - CoverImage = "Special 3", - } - }, - }, - new Volume() - { - Number = 1, - Name = "1", - Chapters = new List() - { - new Chapter() - { - IsSpecial = false, - Number = "0", - CoverImage = "Volume 1", - }, - - }, - } - } - }; + var series = new SeriesBuilder("Test 1") + .WithFormat(MangaFormat.Archive) + .WithVolume(new VolumeBuilder("0") + .WithName(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume) + .WithChapter(new ChapterBuilder("2.5") + .WithIsSpecial(false) + .WithCoverImage("Special 1") + .Build()) + .WithChapter(new ChapterBuilder("2") + .WithIsSpecial(false) + .WithCoverImage("Special 2") + .Build()) + .WithChapter(new ChapterBuilder("0") + .WithIsSpecial(true) + .WithCoverImage("Special 3") + .Build()) + .Build()) + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("0") + .WithIsSpecial(false) + .WithCoverImage("Volume 1") + .Build()) + .Build()) + .Build(); foreach (var vol in series.Volumes) { diff --git a/API.Tests/Helpers/Builders/ChapterBuilder.cs b/API.Tests/Helpers/Builders/ChapterBuilder.cs new file mode 100644 index 000000000..00ad694a5 --- /dev/null +++ b/API.Tests/Helpers/Builders/ChapterBuilder.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using API.Data; +using API.Entities; +using API.Entities.Enums; + +namespace API.Tests.Helpers.Builders; + +public class ChapterBuilder : IEntityBuilder +{ + private Chapter _chapter; + public Chapter Build() => _chapter; + + public ChapterBuilder(string number, string? range=null) + { + _chapter = new Chapter() + { + Range = string.IsNullOrEmpty(range) ? number : range, + Title = string.IsNullOrEmpty(range) ? number : range, + Number = API.Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(number) + string.Empty, + Files = new List(), + Pages = 1 + }; + } + + public ChapterBuilder WithAgeRating(AgeRating rating) + { + _chapter.AgeRating = rating; + return this; + } + + public ChapterBuilder WithPages(int pages) + { + _chapter.Pages = pages; + return this; + } + public ChapterBuilder WithCoverImage(string cover) + { + _chapter.CoverImage = cover; + return this; + } + public ChapterBuilder WithIsSpecial(bool isSpecial) + { + _chapter.IsSpecial = isSpecial; + return this; + } + public ChapterBuilder WithTitle(string title) + { + _chapter.Title = title; + return this; + } + + public ChapterBuilder WithFile(MangaFile file) + { + _chapter.Files ??= new List(); + _chapter.Files.Add(file); + return this; + } +} diff --git a/API.Tests/Helpers/Builders/EntityBuilder.cs b/API.Tests/Helpers/Builders/EntityBuilder.cs new file mode 100644 index 000000000..515e4b934 --- /dev/null +++ b/API.Tests/Helpers/Builders/EntityBuilder.cs @@ -0,0 +1,6 @@ +namespace API.Tests.Helpers.Builders; + +public interface IEntityBuilder +{ + public T Build(); +} diff --git a/API.Tests/Helpers/Builders/SeriesBuilder.cs b/API.Tests/Helpers/Builders/SeriesBuilder.cs new file mode 100644 index 000000000..253341403 --- /dev/null +++ b/API.Tests/Helpers/Builders/SeriesBuilder.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Linq; +using API.Entities; +using API.Entities.Enums; +using API.Entities.Metadata; +using API.Extensions; + +namespace API.Tests.Helpers.Builders; + +public class SeriesBuilder : IEntityBuilder +{ + private readonly Series _series; + public Series Build() + { + _series.Pages = _series.Volumes.Sum(v => v.Chapters.Sum(c => c.Pages)); + return _series; + } + + public SeriesBuilder(string name) + { + _series = new Series() + { + Name = name, + LocalizedName = name.ToNormalized(), + OriginalName = name, + SortName = name, + NormalizedName = name.ToNormalized(), + NormalizedLocalizedName = name.ToNormalized(), + Metadata = new SeriesMetadata(), + Volumes = new List() + }; + } + + public SeriesBuilder WithLocalizedName(string localizedName) + { + _series.LocalizedName = localizedName; + _series.NormalizedLocalizedName = localizedName.ToNormalized(); + return this; + } + + public SeriesBuilder WithFormat(MangaFormat format) + { + _series.Format = format; + return this; + } + + public SeriesBuilder WithVolume(Volume volume) + { + _series.Volumes ??= new List(); + _series.Volumes.Add(volume); + return this; + } + + public SeriesBuilder WithVolumes(List volumes) + { + _series.Volumes = volumes; + return this; + } + + public SeriesBuilder WithMetadata(SeriesMetadata metadata) + { + _series.Metadata = metadata; + return this; + } + + public SeriesBuilder WithPages(int pages) + { + _series.Pages = pages; + return this; + } +} diff --git a/API.Tests/Helpers/Builders/SeriesMetadataBuilder.cs b/API.Tests/Helpers/Builders/SeriesMetadataBuilder.cs new file mode 100644 index 000000000..d84ad152b --- /dev/null +++ b/API.Tests/Helpers/Builders/SeriesMetadataBuilder.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using API.Data; +using API.Entities; +using API.Entities.Enums; +using API.Entities.Metadata; + +namespace API.Tests.Helpers.Builders; + +public class SeriesMetadataBuilder : IEntityBuilder +{ + private SeriesMetadata _seriesMetadata; + public SeriesMetadata Build() => _seriesMetadata; + + public SeriesMetadataBuilder() + { + _seriesMetadata = new SeriesMetadata() + { + CollectionTags = new List(), + Genres = new List(), + Tags = new List(), + People = new List() + }; + } + + public SeriesMetadataBuilder WithCollectionTag(CollectionTag tag) + { + _seriesMetadata.CollectionTags ??= new List(); + _seriesMetadata.CollectionTags.Add(tag); + return this; + } + public SeriesMetadataBuilder WithPublicationStatus(PublicationStatus status) + { + _seriesMetadata.PublicationStatus = status; + return this; + } + +} diff --git a/API.Tests/Helpers/Builders/VolumeBuilder.cs b/API.Tests/Helpers/Builders/VolumeBuilder.cs new file mode 100644 index 000000000..eb074a9df --- /dev/null +++ b/API.Tests/Helpers/Builders/VolumeBuilder.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Linq; +using API.Data; +using API.Entities; + +namespace API.Tests.Helpers.Builders; + +public class VolumeBuilder : IEntityBuilder +{ + private readonly Volume _volume; + public Volume Build() => _volume; + + public VolumeBuilder(string volumeNumber) + { + _volume = DbFactory.Volume(volumeNumber); + } + + public VolumeBuilder WithName(string name) + { + _volume.Name = name; + return this; + } + + public VolumeBuilder WithNumber(int number) + { + _volume.Number = number; + return this; + } + + public VolumeBuilder WithChapters(List chapters) + { + _volume.Chapters = chapters; + return this; + } + + public VolumeBuilder WithChapter(Chapter chapter) + { + _volume.Chapters ??= new List(); + _volume.Chapters.Add(chapter); + _volume.Pages = _volume.Chapters.Sum(c => c.Pages); + return this; + } +} diff --git a/API.Tests/Helpers/CacheHelperTests.cs b/API.Tests/Helpers/CacheHelperTests.cs index d78ed1601..101c2d21d 100644 --- a/API.Tests/Helpers/CacheHelperTests.cs +++ b/API.Tests/Helpers/CacheHelperTests.cs @@ -156,6 +156,8 @@ public class CacheHelperTests var chapter = new Chapter() { + Number = "1", + Range = "1", Created = filesystemFile.LastWriteTime.DateTime, LastModified = filesystemFile.LastWriteTime.DateTime }; @@ -186,6 +188,8 @@ public class CacheHelperTests var chapter = new Chapter() { + Number = "1", + Range = "1", Created = filesystemFile.LastWriteTime.DateTime, LastModified = filesystemFile.LastWriteTime.DateTime }; @@ -216,6 +220,8 @@ public class CacheHelperTests var chapter = new Chapter() { + Number = "1", + Range = "1", Created = filesystemFile.LastWriteTime.DateTime, LastModified = filesystemFile.LastWriteTime.DateTime }; @@ -247,6 +253,8 @@ public class CacheHelperTests var chapter = new Chapter() { + Number = "1", + Range = "1", Created = DateTime.Now.Subtract(TimeSpan.FromMinutes(10)), LastModified = DateTime.Now.Subtract(TimeSpan.FromMinutes(10)) }; @@ -277,6 +285,8 @@ public class CacheHelperTests var chapter = new Chapter() { + Number = "1", + Range = "1", Created = DateTime.Now.Subtract(TimeSpan.FromMinutes(10)), LastModified = DateTime.Now }; diff --git a/API.Tests/Helpers/EntityFactory.cs b/API.Tests/Helpers/EntityFactory.cs index a73a611e2..d8b789569 100644 --- a/API.Tests/Helpers/EntityFactory.cs +++ b/API.Tests/Helpers/EntityFactory.cs @@ -4,6 +4,7 @@ using API.Data; using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; +using API.Extensions; namespace API.Tests.Helpers; @@ -19,7 +20,9 @@ public static class EntityFactory Name = name, SortName = name, LocalizedName = name, - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize(name), + NormalizedName = name.ToNormalized(), + OriginalName = name, + NormalizedLocalizedName = name.ToNormalized(), Volumes = new List(), Metadata = new SeriesMetadata() }; @@ -38,7 +41,7 @@ public static class EntityFactory }; } - public static Chapter CreateChapter(string range, bool isSpecial, List files = null, int pageCount = 0) + public static Chapter CreateChapter(string range, bool isSpecial, List files = null, int pageCount = 0, string title = null) { return new Chapter() { @@ -47,7 +50,7 @@ public static class EntityFactory Number = API.Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(range) + string.Empty, Files = files ?? new List(), Pages = pageCount, - + Title = title ?? range }; } @@ -66,3 +69,6 @@ public static class EntityFactory return DbFactory.CollectionTag(id, title, summary, promoted); } } + + + diff --git a/API.Tests/Helpers/ParserInfoFactory.cs b/API.Tests/Helpers/ParserInfoFactory.cs index 793b764b0..e8c13aa76 100644 --- a/API.Tests/Helpers/ParserInfoFactory.cs +++ b/API.Tests/Helpers/ParserInfoFactory.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using API.Entities.Enums; +using API.Extensions; using API.Parser; using API.Services.Tasks.Scanner; @@ -29,12 +30,12 @@ public static class ParserInfoFactory public static void AddToParsedInfo(IDictionary> collectedSeries, ParserInfo info) { var existingKey = collectedSeries.Keys.FirstOrDefault(ps => - ps.Format == info.Format && ps.NormalizedName == API.Services.Tasks.Scanner.Parser.Parser.Normalize(info.Series)); + ps.Format == info.Format && ps.NormalizedName == info.Series.ToNormalized()); existingKey ??= new ParsedSeries() { Format = info.Format, Name = info.Series, - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize(info.Series) + NormalizedName = info.Series.ToNormalized() }; if (collectedSeries.GetType() == typeof(ConcurrentDictionary<,>)) { diff --git a/API.Tests/Helpers/ParserInfoHelperTests.cs b/API.Tests/Helpers/ParserInfoHelperTests.cs index e51362b81..581b3392c 100644 --- a/API.Tests/Helpers/ParserInfoHelperTests.cs +++ b/API.Tests/Helpers/ParserInfoHelperTests.cs @@ -2,9 +2,11 @@ using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; +using API.Extensions; using API.Helpers; using API.Parser; using API.Services.Tasks.Scanner; +using API.Tests.Helpers.Builders; using Xunit; namespace API.Tests.Helpers; @@ -21,23 +23,14 @@ public class ParserInfoHelperTests 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 Series() - { - Name = "Darker Than Black", - LocalizedName = "Darker Than Black", - OriginalName = "Darker Than Black", - Volumes = new List() - { - new Volume() - { - Number = 1, - Name = "1" - } - }, - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Darker Than Black"), - Metadata = new SeriesMetadata(), - Format = MangaFormat.Epub - }; + var series = new SeriesBuilder("Darker Than Black") + .WithFormat(MangaFormat.Epub) + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithName("1") + .Build()) + .WithLocalizedName("Darker Than Black") + .Build(); Assert.False(ParserInfoHelpers.SeriesHasMatchingParserInfoFormat(series, infos)); } @@ -50,23 +43,15 @@ public class ParserInfoHelperTests 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 Series() - { - Name = "Darker Than Black", - LocalizedName = "Darker Than Black", - OriginalName = "Darker Than Black", - Volumes = new List() - { - new Volume() - { - Number = 1, - Name = "1" - } - }, - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Darker Than Black"), - Metadata = new SeriesMetadata(), - Format = MangaFormat.Epub - }; + + var series = new SeriesBuilder("Darker Than Black") + .WithFormat(MangaFormat.Epub) + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithName("1") + .Build()) + .WithLocalizedName("Darker Than Black") + .Build(); Assert.True(ParserInfoHelpers.SeriesHasMatchingParserInfoFormat(series, infos)); } diff --git a/API.Tests/Helpers/PersonHelperTests.cs b/API.Tests/Helpers/PersonHelperTests.cs index 85f1687c9..d9f5cdb82 100644 --- a/API.Tests/Helpers/PersonHelperTests.cs +++ b/API.Tests/Helpers/PersonHelperTests.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using API.Data; using API.Entities; using API.Entities.Enums; diff --git a/API.Tests/Helpers/SeriesHelperTests.cs b/API.Tests/Helpers/SeriesHelperTests.cs index 139803e0a..d2b998404 100644 --- a/API.Tests/Helpers/SeriesHelperTests.cs +++ b/API.Tests/Helpers/SeriesHelperTests.cs @@ -3,6 +3,7 @@ using System.Linq; using API.Data; using API.Entities; using API.Entities.Enums; +using API.Extensions; using API.Helpers; using API.Services.Tasks.Scanner; using Xunit; @@ -22,21 +23,35 @@ public class SeriesHelperTests { Format = MangaFormat.Archive, Name = "Darker than Black", - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Darker than Black") + NormalizedName = "Darker than Black".ToNormalized() })); Assert.True(SeriesHelper.FindSeries(series, new ParsedSeries() { Format = MangaFormat.Archive, Name = "Darker than Black".ToLower(), - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Darker than Black") + NormalizedName = "Darker than Black".ToNormalized() })); Assert.True(SeriesHelper.FindSeries(series, new ParsedSeries() { Format = MangaFormat.Archive, Name = "Darker than Black".ToUpper(), - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Darker than Black") + NormalizedName = "Darker than Black".ToNormalized() + })); + } + + [Fact] + public void FindSeries_ShouldFind_NullName() + { + var series = DbFactory.Series("Darker than Black"); + series.OriginalName = null; + series.Format = MangaFormat.Archive; + Assert.True(SeriesHelper.FindSeries(series, new ParsedSeries() + { + Format = MangaFormat.Archive, + Name = "Darker than Black", + NormalizedName = "Darker than Black".ToNormalized() })); } @@ -50,21 +65,21 @@ public class SeriesHelperTests { Format = MangaFormat.Image, Name = "Darker than Black", - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Darker than Black") + NormalizedName = "Darker than Black".ToNormalized() })); Assert.False(SeriesHelper.FindSeries(series, new ParsedSeries() { Format = MangaFormat.Image, Name = "Darker than Black".ToLower(), - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Darker than Black") + NormalizedName = "Darker than Black".ToNormalized() })); Assert.False(SeriesHelper.FindSeries(series, new ParsedSeries() { Format = MangaFormat.Image, Name = "Darker than Black".ToUpper(), - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Darker than Black") + NormalizedName = "Darker than Black".ToNormalized() })); } @@ -78,28 +93,28 @@ public class SeriesHelperTests { Format = MangaFormat.Image, Name = "Something Random", - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Something Random") + NormalizedName = "Something Random".ToNormalized() })); Assert.True(SeriesHelper.FindSeries(series, new ParsedSeries() { Format = MangaFormat.Image, Name = "Something Random".ToLower(), - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Something Random") + NormalizedName = "Something Random".ToNormalized() })); Assert.True(SeriesHelper.FindSeries(series, new ParsedSeries() { Format = MangaFormat.Image, Name = "Something Random".ToUpper(), - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Something Random") + NormalizedName = "Something Random".ToNormalized() })); Assert.True(SeriesHelper.FindSeries(series, new ParsedSeries() { Format = MangaFormat.Image, Name = "SomethingRandom".ToUpper(), - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("SomethingRandom") + NormalizedName = "SomethingRandom".ToNormalized() })); } @@ -113,28 +128,28 @@ public class SeriesHelperTests { Format = MangaFormat.Image, Name = "Something Random", - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Something Random") + NormalizedName = "Something Random".ToNormalized() })); Assert.True(SeriesHelper.FindSeries(series, new ParsedSeries() { Format = MangaFormat.Image, Name = "Something Random".ToLower(), - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Something Random") + NormalizedName = "Something Random".ToNormalized() })); Assert.True(SeriesHelper.FindSeries(series, new ParsedSeries() { Format = MangaFormat.Image, Name = "Something Random".ToUpper(), - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Something Random") + NormalizedName = "Something Random".ToNormalized() })); Assert.True(SeriesHelper.FindSeries(series, new ParsedSeries() { Format = MangaFormat.Image, Name = "SomethingRandom".ToUpper(), - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("SomethingRandom") + NormalizedName = "SomethingRandom".ToNormalized() })); } @@ -148,14 +163,14 @@ public class SeriesHelperTests { Format = MangaFormat.Archive, Name = "My Dress-Up Darling", - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("My Dress-Up Darling") + NormalizedName = "My Dress-Up Darling".ToNormalized() })); Assert.True(SeriesHelper.FindSeries(series, new ParsedSeries() { Format = MangaFormat.Archive, Name = "Sono Bisque Doll wa Koi wo Suru".ToLower(), - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Sono Bisque Doll wa Koi wo Suru") + NormalizedName = "Sono Bisque Doll wa Koi wo Suru".ToNormalized() })); } #endregion diff --git a/API.Tests/Parser/ComicParserTests.cs b/API.Tests/Parser/ComicParserTests.cs index 9b6bf212d..72129a777 100644 --- a/API.Tests/Parser/ComicParserTests.cs +++ b/API.Tests/Parser/ComicParserTests.cs @@ -1,5 +1,4 @@ using System.IO.Abstractions.TestingHelpers; -using API.Parser; using API.Services; using API.Services.Tasks.Scanner.Parser; using Microsoft.Extensions.Logging; diff --git a/API.Tests/Parser/DefaultParserTests.cs b/API.Tests/Parser/DefaultParserTests.cs index 7f843b552..541081d0d 100644 --- a/API.Tests/Parser/DefaultParserTests.cs +++ b/API.Tests/Parser/DefaultParserTests.cs @@ -51,7 +51,7 @@ public class DefaultParserTests { const string rootDirectory = "/manga/"; var tokens = expectedParseInfo.Split("~"); - var actual = new ParserInfo {Chapters = "0", Volumes = "0"}; + var actual = new ParserInfo {Series = "", Chapters = "0", Volumes = "0"}; _defaultParser.ParseFromFallbackFolders(inputFile, rootDirectory, LibraryType.Manga, ref actual); Assert.Equal(tokens[0], actual.Series); Assert.Equal(tokens[1], actual.Volumes); diff --git a/API.Tests/Parser/MangaParserTests.cs b/API.Tests/Parser/MangaParserTests.cs index 20c1a27ae..608a08911 100644 --- a/API.Tests/Parser/MangaParserTests.cs +++ b/API.Tests/Parser/MangaParserTests.cs @@ -1,4 +1,3 @@ -using System.Runtime.InteropServices; using API.Entities.Enums; using Xunit; using Xunit.Abstractions; diff --git a/API.Tests/Parser/ParserTest.cs b/API.Tests/Parser/ParserTest.cs index e2f06465b..886643893 100644 --- a/API.Tests/Parser/ParserTest.cs +++ b/API.Tests/Parser/ParserTest.cs @@ -249,7 +249,7 @@ public class ParserTests [InlineData("The ()quick brown fox jumps over the lazy dog")] [InlineData("The (quick (brown)) fox jumps over the lazy dog")] [InlineData("The (quick (brown) fox jumps over the lazy dog)")] - public void BalancedParenTestMatches(string input) + public void BalancedParenTest_Matches(string input) { Assert.Matches($@"^{BalancedParen}$", input); } @@ -261,7 +261,7 @@ public class ParserTests [InlineData("The quick (brown)) fox jumps over the lazy dog")] [InlineData("The quick (brown) fox jumps over the lazy dog)")] [InlineData("(The ))(quick (brown) fox jumps over the lazy dog")] - public void BalancedParenTestDoesNotMatch(string input) + public void BalancedParenTest_DoesNotMatch(string input) { Assert.DoesNotMatch($@"^{BalancedParen}$", input); } @@ -273,9 +273,9 @@ public class ParserTests [InlineData("The []quick brown fox jumps over the lazy dog")] [InlineData("The [quick [brown]] fox jumps over the lazy dog")] [InlineData("The [quick [brown] fox jumps over the lazy dog]")] - public void BalancedBrackTestMatches(string input) + public void BalancedBracketTest_Matches(string input) { - Assert.Matches($@"^{BalancedBrack}$", input); + Assert.Matches($@"^{BalancedBracket}$", input); } [Theory] @@ -285,8 +285,8 @@ public class ParserTests [InlineData("The quick [brown]] fox jumps over the lazy dog")] [InlineData("The quick [brown] fox jumps over the lazy dog]")] [InlineData("[The ]][quick [brown] fox jumps over the lazy dog")] - public void BalancedBrackTestDoesNotMatch(string input) + public void BalancedBracketTest_DoesNotMatch(string input) { - Assert.DoesNotMatch($@"^{BalancedBrack}$", input); + Assert.DoesNotMatch($@"^{BalancedBracket}$", input); } } diff --git a/API.Tests/Repository/CollectionTagRepositoryTests.cs b/API.Tests/Repository/CollectionTagRepositoryTests.cs new file mode 100644 index 000000000..b60b8c9ec --- /dev/null +++ b/API.Tests/Repository/CollectionTagRepositoryTests.cs @@ -0,0 +1,177 @@ +using System.Collections.Generic; +using System.Data.Common; +using System.IO.Abstractions.TestingHelpers; +using System.Linq; +using System.Threading.Tasks; +using API.Data; +using API.Entities; +using API.Entities.Enums; +using API.Helpers; +using API.Services; +using AutoMapper; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace API.Tests.Repository; + +public class CollectionTagRepositoryTests +{ + private readonly IUnitOfWork _unitOfWork; + + private readonly DbConnection _connection; + private readonly DataContext _context; + + private const string CacheDirectory = "C:/kavita/config/cache/"; + private const string CoverImageDirectory = "C:/kavita/config/covers/"; + private const string BackupDirectory = "C:/kavita/config/backups/"; + private const string DataDirectory = "C:/data/"; + + public CollectionTagRepositoryTests() + { + var contextOptions = new DbContextOptionsBuilder().UseSqlite(CreateInMemoryDatabase()).Options; + _connection = RelationalOptionsExtension.Extract(contextOptions).Connection; + + _context = new DataContext(contextOptions); + Task.Run(SeedDb).GetAwaiter().GetResult(); + + var config = new MapperConfiguration(cfg => cfg.AddProfile()); + var mapper = config.CreateMapper(); + _unitOfWork = new UnitOfWork(_context, mapper, null); + } + + #region Setup + + private static DbConnection CreateInMemoryDatabase() + { + var connection = new SqliteConnection("Filename=:memory:"); + + connection.Open(); + + return connection; + } + + private async Task SeedDb() + { + await _context.Database.MigrateAsync(); + var filesystem = CreateFileSystem(); + + await Seed.SeedSettings(_context, + new DirectoryService(Substitute.For>(), filesystem)); + + var setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.CacheDirectory).SingleAsync(); + setting.Value = CacheDirectory; + + setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BackupDirectory).SingleAsync(); + setting.Value = BackupDirectory; + + _context.ServerSetting.Update(setting); + + var lib = new Library() + { + Name = "Manga", Folders = new List() {new FolderPath() {Path = "C:/data/"}} + }; + + _context.AppUser.Add(new AppUser() + { + UserName = "majora2007", + Libraries = new List() + { + lib + } + }); + + return await _context.SaveChangesAsync() > 0; + } + + private async Task ResetDb() + { + _context.Series.RemoveRange(_context.Series.ToList()); + _context.AppUserRating.RemoveRange(_context.AppUserRating.ToList()); + _context.Genre.RemoveRange(_context.Genre.ToList()); + _context.CollectionTag.RemoveRange(_context.CollectionTag.ToList()); + _context.Person.RemoveRange(_context.Person.ToList()); + + await _context.SaveChangesAsync(); + } + + private static MockFileSystem CreateFileSystem() + { + var fileSystem = new MockFileSystem(); + fileSystem.Directory.SetCurrentDirectory("C:/kavita/"); + fileSystem.AddDirectory("C:/kavita/config/"); + fileSystem.AddDirectory(CacheDirectory); + fileSystem.AddDirectory(CoverImageDirectory); + fileSystem.AddDirectory(BackupDirectory); + fileSystem.AddDirectory(DataDirectory); + + return fileSystem; + } + + #endregion + + #region RemoveTagsWithoutSeries + + [Fact] + public async Task RemoveTagsWithoutSeries_ShouldRemoveTags() + { + var library = DbFactory.Library("Test", LibraryType.Manga); + var series = DbFactory.Series("Test 1"); + var commonTag = DbFactory.CollectionTag(0, "Tag 1"); + series.Metadata.CollectionTags.Add(commonTag); + series.Metadata.CollectionTags.Add(DbFactory.CollectionTag(0, "Tag 2")); + + var series2 = DbFactory.Series("Test 1"); + series2.Metadata.CollectionTags.Add(commonTag); + library.Series.Add(series); + library.Series.Add(series2); + _unitOfWork.LibraryRepository.Add(library); + await _unitOfWork.CommitAsync(); + + Assert.Equal(2, series.Metadata.CollectionTags.Count); + Assert.Single(series2.Metadata.CollectionTags); + + // Delete both series + _unitOfWork.SeriesRepository.Remove(series); + _unitOfWork.SeriesRepository.Remove(series2); + + await _unitOfWork.CommitAsync(); + + // Validate that both tags exist + Assert.Equal(2, (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()).Count()); + + await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries(); + + Assert.Empty(await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()); + } + + [Fact] + public async Task RemoveTagsWithoutSeries_ShouldNotRemoveTags() + { + var library = DbFactory.Library("Test", LibraryType.Manga); + var series = DbFactory.Series("Test 1"); + var commonTag = DbFactory.CollectionTag(0, "Tag 1"); + series.Metadata.CollectionTags.Add(commonTag); + series.Metadata.CollectionTags.Add(DbFactory.CollectionTag(0, "Tag 2")); + + var series2 = DbFactory.Series("Test 1"); + series2.Metadata.CollectionTags.Add(commonTag); + library.Series.Add(series); + library.Series.Add(series2); + _unitOfWork.LibraryRepository.Add(library); + await _unitOfWork.CommitAsync(); + + Assert.Equal(2, series.Metadata.CollectionTags.Count); + Assert.Single(series2.Metadata.CollectionTags); + + await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries(); + + // Validate that both tags exist + Assert.Equal(2, (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()).Count()); + } + + #endregion +} diff --git a/API.Tests/Repository/SeriesRepositoryTests.cs b/API.Tests/Repository/SeriesRepositoryTests.cs index fe285641e..76b1c04a5 100644 --- a/API.Tests/Repository/SeriesRepositoryTests.cs +++ b/API.Tests/Repository/SeriesRepositoryTests.cs @@ -40,7 +40,7 @@ public class SeriesRepositoryTests var config = new MapperConfiguration(cfg => cfg.AddProfile()); var mapper = config.CreateMapper(); - _unitOfWork = new UnitOfWork(_context, mapper, null); + _unitOfWork = new UnitOfWork(_context, mapper, null!); } #region Setup @@ -138,11 +138,13 @@ public class SeriesRepositoryTests } - [InlineData("Heion Sedai no Idaten-tachi", "", MangaFormat.Archive, "The Idaten Deities Know Only Peace")] // Matching on localized name in DB - [InlineData("Heion Sedai no Idaten-tachi", "", MangaFormat.Pdf, null)] + // This test case isn't ready to go + [InlineData("Heion Sedai no Idaten-tachi", MangaFormat.Archive, "", "The Idaten Deities Know Only Peace")] // Matching on localized name in DB + [InlineData("Heion Sedai no Idaten-tachi", MangaFormat.Pdf, "", null)] public async Task GetFullSeriesByAnyName_Should(string seriesName, MangaFormat format, string localizedName, string? expected) { - var firstSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); + await ResetDb(); + await SetupSeriesData(); var series = await _unitOfWork.SeriesRepository.GetFullSeriesByAnyName(seriesName, localizedName, 1, format); @@ -157,6 +159,4 @@ public class SeriesRepositoryTests } } - - //public async Task } diff --git a/API.Tests/Services/ArchiveServiceTests.cs b/API.Tests/Services/ArchiveServiceTests.cs index b59ee097e..9974c256c 100644 --- a/API.Tests/Services/ArchiveServiceTests.cs +++ b/API.Tests/Services/ArchiveServiceTests.cs @@ -5,7 +5,6 @@ using System.IO.Abstractions.TestingHelpers; using System.IO.Compression; using System.Linq; using API.Archive; -using API.Data.Metadata; using API.Services; using Microsoft.Extensions.Logging; using NetVips; diff --git a/API.Tests/Services/BackupServiceTests.cs b/API.Tests/Services/BackupServiceTests.cs index 783e0b62d..3fd6d346b 100644 --- a/API.Tests/Services/BackupServiceTests.cs +++ b/API.Tests/Services/BackupServiceTests.cs @@ -1,18 +1,15 @@ using System.Collections.Generic; using System.Data.Common; using System.IO.Abstractions.TestingHelpers; -using System.IO.Compression; using System.Linq; using System.Threading.Tasks; using API.Data; using API.Entities; using API.Entities.Enums; -using API.Extensions; using API.Services; using API.Services.Tasks; using API.SignalR; using AutoMapper; -using Microsoft.AspNetCore.SignalR; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; diff --git a/API.Tests/Services/BookmarkServiceTests.cs b/API.Tests/Services/BookmarkServiceTests.cs index 97c07a281..108d95d4b 100644 --- a/API.Tests/Services/BookmarkServiceTests.cs +++ b/API.Tests/Services/BookmarkServiceTests.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Data.Common; using System.IO; using System.IO.Abstractions.TestingHelpers; @@ -10,9 +9,12 @@ using API.Data.Repositories; using API.DTOs.Reader; using API.Entities; using API.Entities.Enums; +using API.Entities.Metadata; +using API.Extensions; using API.Helpers; using API.Services; using API.SignalR; +using API.Tests.Helpers.Builders; using AutoMapper; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; @@ -136,27 +138,22 @@ public class BookmarkServiceTests // Delete all Series to reset state await ResetDB(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithFormat(MangaFormat.Epub) + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("1") + .Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - new Volume() - { - Chapters = new List() - { - new Chapter() - { + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); - } - } - } - } - }); _context.AppUser.Add(new AppUser() { @@ -194,27 +191,22 @@ public class BookmarkServiceTests // Delete all Series to reset state await ResetDB(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithFormat(MangaFormat.Epub) + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("0") + .Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - new Volume() - { - Chapters = new List() - { - new Chapter() - { + Name = "Test LIb", + Type = LibraryType.Manga, + }; - } - } - } - } - }); + _context.Series.Add(series); _context.AppUser.Add(new AppUser() @@ -270,28 +262,22 @@ public class BookmarkServiceTests // Delete all Series to reset state await ResetDB(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithFormat(MangaFormat.Epub) + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("1") + .Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - new Volume() - { - Chapters = new List() - { - new Chapter() - { - - } - } - } - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -342,7 +328,7 @@ public class BookmarkServiceTests Assert.Equal(2, ds.GetFiles(BookmarkDirectory, searchOption:SearchOption.AllDirectories).Count()); - Assert.False(ds.FileSystem.FileInfo.FromFileName(Path.Join(BookmarkDirectory, "1/1/1/0001.jpg")).Exists); + Assert.False(ds.FileSystem.FileInfo.New(Path.Join(BookmarkDirectory, "1/1/1/0001.jpg")).Exists); } #endregion @@ -357,27 +343,23 @@ public class BookmarkServiceTests // Delete all Series to reset state await ResetDB(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithFormat(MangaFormat.Epub) + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("1") + .Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - new Volume() - { - Chapters = new List() - { - new Chapter() - { + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); - } - } - } - } - }); _context.AppUser.Add(new AppUser() { @@ -419,28 +401,22 @@ public class BookmarkServiceTests // Delete all Series to reset state await ResetDB(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithFormat(MangaFormat.Epub) + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("1") + .Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - new Volume() - { - Chapters = new List() - { - new Chapter() - { - - } - } - } - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -483,27 +459,19 @@ public class BookmarkServiceTests // Delete all Series to reset state await ResetDB(); - var series = new Series() + var series = new SeriesBuilder("Test") + .WithFormat(MangaFormat.Epub) + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("1") + .Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() - { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - new Volume() - { - Chapters = new List() - { - new Chapter() - { - - } - } - } - } + Name = "Test LIb", + Type = LibraryType.Manga, }; _context.Series.Add(series); @@ -528,7 +496,7 @@ public class BookmarkServiceTests await _context.SaveChangesAsync(); var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Bookmarks); - Assert.NotEmpty(user.Bookmarks); + Assert.NotEmpty(user!.Bookmarks); series.Volumes = new List(); _unitOfWork.SeriesRepository.Update(series); diff --git a/API.Tests/Services/CacheServiceTests.cs b/API.Tests/Services/CacheServiceTests.cs index 6d973aecf..7e9b3c355 100644 --- a/API.Tests/Services/CacheServiceTests.cs +++ b/API.Tests/Services/CacheServiceTests.cs @@ -171,6 +171,7 @@ public class CacheServiceTests var c = new Chapter() { Number = "1", + Range = "1", Files = new List() { new MangaFile() @@ -272,6 +273,8 @@ public class CacheServiceTests var c = new Chapter() { + Number = "1", + Range = "1", Files = new List() { new MangaFile() @@ -303,6 +306,8 @@ public class CacheServiceTests var c = new Chapter() { Id = 1, + Number = "1", + Range = "1", Files = new List() }; @@ -340,6 +345,8 @@ public class CacheServiceTests var c = new Chapter() { Id = 1, + Number = "1", + Range = "1", Files = new List() { new MangaFile() @@ -392,6 +399,8 @@ public class CacheServiceTests var c = new Chapter() { Id = 1, + Number = "1", + Range = "1", Files = new List() { new MangaFile() @@ -440,6 +449,8 @@ public class CacheServiceTests var c = new Chapter() { Id = 1, + Number = "1", + Range = "1", Files = new List() { new MangaFile() diff --git a/API.Tests/Services/CleanupServiceTests.cs b/API.Tests/Services/CleanupServiceTests.cs index 84e4d5fd5..177376bd9 100644 --- a/API.Tests/Services/CleanupServiceTests.cs +++ b/API.Tests/Services/CleanupServiceTests.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Data.Common; using System.IO; using System.IO.Abstractions.TestingHelpers; using System.Linq; @@ -8,21 +7,16 @@ using System.Threading.Tasks; using API.Data; using API.Data.Repositories; using API.DTOs.Filtering; -using API.DTOs.Settings; using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; +using API.Extensions; using API.Helpers; -using API.Helpers.Converters; using API.Services; using API.Services.Tasks; using API.SignalR; using API.Tests.Helpers; -using AutoMapper; -using Microsoft.AspNetCore.SignalR; -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; +using API.Tests.Helpers.Builders; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -149,6 +143,8 @@ public class CleanupServiceTests : AbstractDbTest var v = DbFactory.Volume("1"); v.Chapters.Add(new Chapter() { + Number = "0", + Range = "0", CoverImage = "v01_c01.jpg" }); v.CoverImage = "v01_c01.jpg"; @@ -161,6 +157,8 @@ public class CleanupServiceTests : AbstractDbTest v = DbFactory.Volume("1"); v.Chapters.Add(new Chapter() { + Number = "0", + Range = "0", CoverImage = "v01_c03.jpg" }); v.CoverImage = "v01_c03jpg"; @@ -200,6 +198,7 @@ public class CleanupServiceTests : AbstractDbTest s.Metadata.CollectionTags.Add(new CollectionTag() { Title = "Something", + NormalizedTitle = "Something".ToNormalized(), CoverImage = $"{ImageService.GetCollectionTagFormat(1)}.jpg" }); s.CoverImage = $"{ImageService.GetSeriesFormat(1)}.jpg"; @@ -211,6 +210,7 @@ public class CleanupServiceTests : AbstractDbTest s.Metadata.CollectionTags.Add(new CollectionTag() { Title = "Something 2", + NormalizedTitle = "Something 2".ToNormalized(), CoverImage = $"{ImageService.GetCollectionTagFormat(2)}.jpg" }); s.CoverImage = $"{ImageService.GetSeriesFormat(3)}.jpg"; @@ -250,14 +250,16 @@ public class CleanupServiceTests : AbstractDbTest new ReadingList() { Title = "Something", - NormalizedTitle = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Something"), - CoverImage = $"{ImageService.GetReadingListFormat(1)}.jpg" + NormalizedTitle = "Something".ToNormalized(), + CoverImage = $"{ImageService.GetReadingListFormat(1)}.jpg", + AgeRating = AgeRating.Unknown }, new ReadingList() { Title = "Something 2", - NormalizedTitle = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Something 2"), - CoverImage = $"{ImageService.GetReadingListFormat(2)}.jpg" + NormalizedTitle = "Something 2".ToNormalized(), + CoverImage = $"{ImageService.GetReadingListFormat(2)}.jpg", + AgeRating = AgeRating.Unknown } } }); @@ -408,22 +410,25 @@ public class CleanupServiceTests : AbstractDbTest [Fact] public async Task CleanupDbEntries_CleanupAbandonedChapters() { - var c = EntityFactory.CreateChapter("1", false, new List(), 1); - _context.Series.Add(new Series() + var c = new ChapterBuilder("0") + .WithPages(1) + .Build(); + var series = new SeriesBuilder("Test") + .WithFormat(MangaFormat.Epub) + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithNumber(1) + .WithChapter(c) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - c, - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); + _context.AppUser.Add(new AppUser() { @@ -461,25 +466,19 @@ public class CleanupServiceTests : AbstractDbTest { var c = new CollectionTag() { - Title = "Test Tag" + Title = "Test Tag", + NormalizedTitle = "Test Tag".ToNormalized(), }; - var s = new Series() + var s = new SeriesBuilder("Test") + .WithFormat(MangaFormat.Epub) + .WithMetadata(new SeriesMetadataBuilder().WithCollectionTag(c).Build()) + .Build(); + s.Library = new Library() { - Name = "Test", - Library = new Library() - { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List(), - Metadata = new SeriesMetadata() - { - CollectionTags = new List() - { - c - } - } + Name = "Test LIb", + Type = LibraryType.Manga, }; + _context.Series.Add(s); _context.AppUser.Add(new AppUser() @@ -511,19 +510,14 @@ public class CleanupServiceTests : AbstractDbTest { await ResetDb(); - var s = new Series() + var s = new SeriesBuilder("Test CleanupWantToRead_ShouldRemoveFullyReadSeries") + .WithMetadata(new SeriesMetadataBuilder().WithPublicationStatus(PublicationStatus.Completed).Build()) + .Build(); + + s.Library = new Library() { - Name = "Test CleanupWantToRead_ShouldRemoveFullyReadSeries", - Library = new Library() - { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List(), - Metadata = new SeriesMetadata() - { - PublicationStatus = PublicationStatus.Completed - } + Name = "Test LIb", + Type = LibraryType.Manga, }; _context.Series.Add(s); diff --git a/API.Tests/Services/CollectionTagServiceTests.cs b/API.Tests/Services/CollectionTagServiceTests.cs index bb8da9ad0..33aa36b29 100644 --- a/API.Tests/Services/CollectionTagServiceTests.cs +++ b/API.Tests/Services/CollectionTagServiceTests.cs @@ -2,14 +2,13 @@ using System.Linq; using System.Threading.Tasks; using API.Data; +using API.Data.Repositories; using API.DTOs.CollectionTags; using API.Entities; using API.Entities.Enums; using API.Services; -using API.Services.Tasks.Metadata; using API.SignalR; using API.Tests.Helpers; -using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -87,7 +86,7 @@ public class CollectionTagServiceTests : AbstractDbTest { await SeedSeries(); var ids = new[] {1, 2}; - await _service.AddTagToSeries(await _unitOfWork.CollectionTagRepository.GetFullTagAsync(1), ids); + await _service.AddTagToSeries(await _unitOfWork.CollectionTagRepository.GetTagAsync(1, CollectionTagIncludes.SeriesMetadata), ids); var metadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIdsAsync(ids); Assert.True(metadatas.ElementAt(0).CollectionTags.Any(t => t.Title.Equals("Tag 1"))); @@ -99,7 +98,7 @@ public class CollectionTagServiceTests : AbstractDbTest { await SeedSeries(); var ids = new[] {1, 2}; - var tag = await _unitOfWork.CollectionTagRepository.GetFullTagAsync(2); + var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(2, CollectionTagIncludes.SeriesMetadata); await _service.AddTagToSeries(tag, ids); await _service.RemoveTagFromSeries(tag, new[] {1}); diff --git a/API.Tests/Services/DeviceServiceTests.cs b/API.Tests/Services/DeviceServiceTests.cs index 78ec8cfd2..1d021c76d 100644 --- a/API.Tests/Services/DeviceServiceTests.cs +++ b/API.Tests/Services/DeviceServiceTests.cs @@ -5,7 +5,6 @@ using API.DTOs.Device; using API.Entities; using API.Entities.Enums.Device; using API.Services; -using API.Services.Tasks; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; diff --git a/API.Tests/Services/DirectoryServiceTests.cs b/API.Tests/Services/DirectoryServiceTests.cs index 6784b9cb1..cca69398e 100644 --- a/API.Tests/Services/DirectoryServiceTests.cs +++ b/API.Tests/Services/DirectoryServiceTests.cs @@ -152,7 +152,7 @@ public class DirectoryServiceTests var ds = new DirectoryService(Substitute.For>(), fileSystem); var files = ds.GetFiles(testDirectory, API.Services.Tasks.Scanner.Parser.Parser.ArchiveFileExtensions).ToList(); - Assert.Equal(10, files.Count()); + Assert.Equal(10, files.Count); Assert.All(files, s => fileSystem.Path.GetExtension(s).Equals(".zip")); } @@ -171,7 +171,7 @@ public class DirectoryServiceTests var ds = new DirectoryService(Substitute.For>(), fileSystem); var files = ds.GetFiles(testDirectory).ToList(); - Assert.Equal(11, files.Count()); + Assert.Equal(11, files.Count); } [Fact] @@ -189,7 +189,7 @@ public class DirectoryServiceTests var ds = new DirectoryService(Substitute.For>(), fileSystem); var files = ds.GetFiles(testDirectory).ToList(); - Assert.Equal(11, files.Count()); + Assert.Equal(11, files.Count); } [Fact] @@ -207,7 +207,7 @@ public class DirectoryServiceTests var ds = new DirectoryService(Substitute.For>(), fileSystem); var files = ds.GetFiles(testDirectory).ToList(); - Assert.Equal(10, files.Count()); + Assert.Equal(10, files.Count); } [Fact] @@ -225,7 +225,7 @@ public class DirectoryServiceTests var ds = new DirectoryService(Substitute.For>(), fileSystem); var files = ds.GetFiles(testDirectory).ToList(); - Assert.Equal(10, files.Count()); + Assert.Equal(10, files.Count); } [Fact] @@ -243,7 +243,7 @@ public class DirectoryServiceTests var ds = new DirectoryService(Substitute.For>(), fileSystem); var files = ds.GetFiles(testDirectory).ToList(); - Assert.Equal(10, files.Count()); + Assert.Equal(10, files.Count); } [Fact] @@ -325,7 +325,7 @@ public class DirectoryServiceTests ds.CopyFileToDirectory($"{testDirectory}file/data-0.txt", "/manga/output/"); Assert.True(fileSystem.FileExists("/manga/output/data-0.txt")); Assert.True(fileSystem.FileExists("/manga/file/data-0.txt")); - Assert.True(fileSystem.FileInfo.FromFileName("/manga/file/data-0.txt").Length == fileSystem.FileInfo.FromFileName("/manga/output/data-0.txt").Length); + Assert.True(fileSystem.FileInfo.New("/manga/file/data-0.txt").Length == fileSystem.FileInfo.New("/manga/output/data-0.txt").Length); } #endregion @@ -353,7 +353,7 @@ public class DirectoryServiceTests var ds = new DirectoryService(Substitute.For>(), fileSystem); ds.CopyDirectoryToDirectory($"{testDirectory}empty/", "/manga/output/"); - Assert.Empty(fileSystem.DirectoryInfo.FromDirectoryName("/manga/output/").GetFiles()); + Assert.Empty(fileSystem.DirectoryInfo.New("/manga/output/").GetFiles()); } [Fact] @@ -427,7 +427,7 @@ public class DirectoryServiceTests var ds = new DirectoryService(Substitute.For>(), fileSystem); ds.ExistOrCreate("c:/manga/output/"); - Assert.True(ds.FileSystem.DirectoryInfo.FromDirectoryName("c:/manga/output/").Exists); + Assert.True(ds.FileSystem.DirectoryInfo.New("c:/manga/output/").Exists); } #endregion @@ -448,9 +448,9 @@ public class DirectoryServiceTests var ds = new DirectoryService(Substitute.For>(), fileSystem); ds.ClearAndDeleteDirectory($"{testDirectory}"); Assert.Empty(ds.GetFiles("/manga/", searchOption: SearchOption.AllDirectories)); - Assert.Empty(ds.FileSystem.DirectoryInfo.FromDirectoryName("/manga/").GetDirectories()); - Assert.True(ds.FileSystem.DirectoryInfo.FromDirectoryName("/manga/").Exists); - Assert.False(ds.FileSystem.DirectoryInfo.FromDirectoryName("/manga/base").Exists); + Assert.Empty(ds.FileSystem.DirectoryInfo.New("/manga/").GetDirectories()); + Assert.True(ds.FileSystem.DirectoryInfo.New("/manga/").Exists); + Assert.False(ds.FileSystem.DirectoryInfo.New("/manga/base").Exists); } #endregion @@ -470,9 +470,9 @@ public class DirectoryServiceTests var ds = new DirectoryService(Substitute.For>(), fileSystem); ds.ClearDirectory($"{testDirectory}file/"); - Assert.Empty(ds.FileSystem.DirectoryInfo.FromDirectoryName($"{testDirectory}file/").GetDirectories()); - Assert.True(ds.FileSystem.DirectoryInfo.FromDirectoryName("/manga/").Exists); - Assert.True(ds.FileSystem.DirectoryInfo.FromDirectoryName($"{testDirectory}file/").Exists); + Assert.Empty(ds.FileSystem.DirectoryInfo.New($"{testDirectory}file/").GetDirectories()); + Assert.True(ds.FileSystem.DirectoryInfo.New("/manga/").Exists); + Assert.True(ds.FileSystem.DirectoryInfo.New($"{testDirectory}file/").Exists); } [Fact] @@ -487,9 +487,9 @@ public class DirectoryServiceTests var ds = new DirectoryService(Substitute.For>(), fileSystem); ds.ClearDirectory($"{testDirectory}"); - Assert.Empty(ds.FileSystem.DirectoryInfo.FromDirectoryName($"{testDirectory}").GetDirectories()); - Assert.True(ds.FileSystem.DirectoryInfo.FromDirectoryName(testDirectory).Exists); - Assert.False(ds.FileSystem.DirectoryInfo.FromDirectoryName($"{testDirectory}file/").Exists); + Assert.Empty(ds.FileSystem.DirectoryInfo.New($"{testDirectory}").GetDirectories()); + Assert.True(ds.FileSystem.DirectoryInfo.New(testDirectory).Exists); + Assert.False(ds.FileSystem.DirectoryInfo.New($"{testDirectory}file/").Exists); } #endregion @@ -587,7 +587,7 @@ public class DirectoryServiceTests 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(); - Assert.Equal(4, outputFiles.Count()); // we have 2 already there and 2 copies + 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")) @@ -645,10 +645,10 @@ public class DirectoryServiceTests const string testDirectory = "/manga/"; var fileSystem = new MockFileSystem(); fileSystem.AddDirectory($"{testDirectory}dir1"); - var di = fileSystem.DirectoryInfo.FromDirectoryName($"{testDirectory}dir1"); + var di = fileSystem.DirectoryInfo.New($"{testDirectory}dir1"); di.Attributes |= FileAttributes.System; fileSystem.AddDirectory($"{testDirectory}dir2"); - di = fileSystem.DirectoryInfo.FromDirectoryName($"{testDirectory}dir2"); + di = fileSystem.DirectoryInfo.New($"{testDirectory}dir2"); di.Attributes |= FileAttributes.Hidden; fileSystem.AddDirectory($"{testDirectory}dir3"); fileSystem.AddFile($"{testDirectory}file_0.zip", new MockFileData("")); diff --git a/API.Tests/Services/ParseScannedFilesTests.cs b/API.Tests/Services/ParseScannedFilesTests.cs index 5ead303ae..a87632b30 100644 --- a/API.Tests/Services/ParseScannedFilesTests.cs +++ b/API.Tests/Services/ParseScannedFilesTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Data.Common; using System.IO.Abstractions.TestingHelpers; @@ -9,15 +8,13 @@ using API.Data; using API.Data.Metadata; using API.Entities; using API.Entities.Enums; +using API.Extensions; using API.Parser; using API.Services; using API.Services.Tasks.Scanner; using API.Services.Tasks.Scanner.Parser; using API.SignalR; -using API.Tests.Helpers; using AutoMapper; -using DotNet.Globbing; -using Flurl.Util; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -254,7 +251,7 @@ public class ParseScannedFilesTests var foundParsedSeries = new ParsedSeries() { Name = parsedFiles.First().Series, - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize(parsedFiles.First().Series), + NormalizedName = parsedFiles.First().Series.ToNormalized(), Format = parsedFiles.First().Format }; diff --git a/API.Tests/Services/ReaderServiceTests.cs b/API.Tests/Services/ReaderServiceTests.cs index 417a87e42..a9a713209 100644 --- a/API.Tests/Services/ReaderServiceTests.cs +++ b/API.Tests/Services/ReaderServiceTests.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Data.Common; using System.IO.Abstractions.TestingHelpers; using System.Linq; @@ -10,10 +9,13 @@ using API.DTOs; using API.DTOs.Reader; using API.Entities; using API.Entities.Enums; +using API.Entities.Metadata; +using API.Extensions; using API.Helpers; using API.Services; using API.SignalR; using API.Tests.Helpers; +using API.Tests.Helpers.Builders; using AutoMapper; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; @@ -125,27 +127,23 @@ public class ReaderServiceTests { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("0") + .WithPages(1) + .Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - new Volume() - { - Chapters = new List() - { - new Chapter() - { - Pages = 1 - } - } - } - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); + await _context.SaveChangesAsync(); @@ -164,27 +162,22 @@ public class ReaderServiceTests { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("0") + .WithPages(1) + .Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - new Volume() - { - Chapters = new List() - { - new Chapter() - { - Pages = 1 - } - } - } - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -213,27 +206,22 @@ public class ReaderServiceTests { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("0") + .WithPages(1) + .Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - new Volume() - { - Chapters = new List() - { - new Chapter() - { - Pages = 1 - } - } - } - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -279,31 +267,25 @@ public class ReaderServiceTests { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("0") + .WithPages(1) + .Build()) + .WithChapter(new ChapterBuilder("0") + .WithPages(2) + .Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - new Volume() - { - Chapters = new List() - { - new Chapter() - { - Pages = 1 - }, - new Chapter() - { - Pages = 2 - } - } - } - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -329,31 +311,25 @@ public class ReaderServiceTests { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("0") + .WithPages(1) + .Build()) + .WithChapter(new ChapterBuilder("0") + .WithPages(2) + .Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - new Volume() - { - Chapters = new List() - { - new Chapter() - { - Pages = 1 - }, - new Chapter() - { - Pages = 2 - } - } - } - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -388,32 +364,61 @@ public class ReaderServiceTests // V1 -> V2 await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("1").Build()) + .WithChapter(new ChapterBuilder("2").Build()) + .Build()) + + .WithVolume(new VolumeBuilder("2") + .WithNumber(2) + .WithChapter(new ChapterBuilder("21").Build()) + .WithChapter(new ChapterBuilder("22").Build()) + .Build()) + + .WithVolume(new VolumeBuilder("3") + .WithNumber(3) + .WithChapter(new ChapterBuilder("31").Build()) + .WithChapter(new ChapterBuilder("32").Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List()), - EntityFactory.CreateChapter("2", false, new List()), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("21", false, new List()), - EntityFactory.CreateChapter("22", false, new List()), - }), - EntityFactory.CreateVolume("3", new List() - { - EntityFactory.CreateChapter("31", false, new List()), - EntityFactory.CreateChapter("32", false, new List()), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); + + // _context.Series.Add(new Series() + // { + // Name = "Test", + // NormalizedName = "Test".ToNormalized(), + // Library = new Library() { + // Name = "Test LIb", + // Type = LibraryType.Manga, + // }, + // Volumes = new List() + // { + // EntityFactory.CreateVolume("1", new List() + // { + // EntityFactory.CreateChapter("1", false, new List()), + // EntityFactory.CreateChapter("2", false, new List()), + // }), + // EntityFactory.CreateVolume("2", new List() + // { + // EntityFactory.CreateChapter("21", false, new List()), + // EntityFactory.CreateChapter("22", false, new List()), + // }), + // EntityFactory.CreateVolume("3", new List() + // { + // EntityFactory.CreateChapter("31", false, new List()), + // EntityFactory.CreateChapter("32", false, new List()), + // }), + // } + // }); _context.AppUser.Add(new AppUser() { @@ -434,32 +439,34 @@ public class ReaderServiceTests { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("1").Build()) + .WithChapter(new ChapterBuilder("2").Build()) + .Build()) + + .WithVolume(new VolumeBuilder("2") + .WithNumber(2) + .WithChapter(new ChapterBuilder("21").Build()) + .WithChapter(new ChapterBuilder("22").Build()) + .Build()) + + .WithVolume(new VolumeBuilder("3") + .WithNumber(3) + .WithChapter(new ChapterBuilder("31").Build()) + .WithChapter(new ChapterBuilder("32").Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List()), - EntityFactory.CreateChapter("2", false, new List()), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("21", false, new List()), - EntityFactory.CreateChapter("22", false, new List()), - }), - EntityFactory.CreateVolume("3", new List() - { - EntityFactory.CreateChapter("31", false, new List()), - EntityFactory.CreateChapter("32", false, new List()), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); + _context.AppUser.Add(new AppUser() { @@ -481,32 +488,34 @@ public class ReaderServiceTests { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("1").Build()) + .WithChapter(new ChapterBuilder("2").Build()) + .Build()) + + .WithVolume(new VolumeBuilder("1.5") + .WithNumber(2) + .WithChapter(new ChapterBuilder("21").Build()) + .WithChapter(new ChapterBuilder("22").Build()) + .Build()) + + .WithVolume(new VolumeBuilder("3") + .WithNumber(3) + .WithChapter(new ChapterBuilder("31").Build()) + .WithChapter(new ChapterBuilder("32").Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List()), - EntityFactory.CreateChapter("2", false, new List()), - }), - EntityFactory.CreateVolume("1.5", new List() - { - EntityFactory.CreateChapter("21", false, new List()), - EntityFactory.CreateChapter("22", false, new List()), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("31", false, new List()), - EntityFactory.CreateChapter("32", false, new List()), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); + _context.AppUser.Add(new AppUser() { @@ -528,27 +537,27 @@ public class ReaderServiceTests { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("1").Build()) + .WithChapter(new ChapterBuilder("2").Build()) + .Build()) + + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("21").Build()) + .WithChapter(new ChapterBuilder("22").Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("1", false, new List()), - EntityFactory.CreateChapter("2", false, new List()), - }), - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("21", false, new List()), - EntityFactory.CreateChapter("22", false, new List()), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -571,30 +580,32 @@ public class ReaderServiceTests { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("66").Build()) + .WithChapter(new ChapterBuilder("67").Build()) + .Build()) + + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("1").Build()) + .Build()) + + .WithVolume(new VolumeBuilder("2") + .WithNumber(2) + .WithChapter(new ChapterBuilder("0").Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("66", false, new List()), - EntityFactory.CreateChapter("67", false, new List()), - }), - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List()), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("0", false, new List()), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); + _context.AppUser.Add(new AppUser() { @@ -616,27 +627,29 @@ public class ReaderServiceTests { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("1").Build()) + .WithChapter(new ChapterBuilder("2").Build()) + .Build()) + + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) + .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List()), - EntityFactory.CreateChapter("2", false, new List()), - }), - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("A.cbz", true, new List()), - EntityFactory.CreateChapter("B.cbz", true, new List()), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); + + _context.AppUser.Add(new AppUser() { @@ -657,22 +670,21 @@ public class ReaderServiceTests { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("1").Build()) + .WithChapter(new ChapterBuilder("2").Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List()), - EntityFactory.CreateChapter("2", false, new List()), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -693,27 +705,28 @@ public class ReaderServiceTests { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("1").Build()) + .WithChapter(new ChapterBuilder("2").Build()) + .Build()) + + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("1").WithIsSpecial(true).Build()) + .WithChapter(new ChapterBuilder("2").WithIsSpecial(true).Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("1", false, new List()), - EntityFactory.CreateChapter("2", false, new List()), - }), - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List()), - EntityFactory.CreateChapter("2", false, new List()), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); + _context.AppUser.Add(new AppUser() { @@ -734,27 +747,28 @@ public class ReaderServiceTests { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("1").Build()) + .WithChapter(new ChapterBuilder("2").Build()) + .Build()) + + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) + .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List()), - EntityFactory.CreateChapter("2", false, new List()), - }), - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("A.cbz", true, new List()), - EntityFactory.CreateChapter("B.cbz", true, new List()), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); + _context.AppUser.Add(new AppUser() { @@ -777,23 +791,22 @@ public class ReaderServiceTests { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("1").Build()) + .WithChapter(new ChapterBuilder("2").Build()) + .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("1", false, new List()), - EntityFactory.CreateChapter("2", false, new List()), - EntityFactory.CreateChapter("A.cbz", true, new List()), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -816,27 +829,26 @@ public class ReaderServiceTests { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("1").Build()) + .WithChapter(new ChapterBuilder("2").Build()) + .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) + .Build()) + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("0").Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("1", false, new List()), - EntityFactory.CreateChapter("2", false, new List()), - EntityFactory.CreateChapter("A.cbz", true, new List()), - }), - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("0", false, new List()), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -858,27 +870,26 @@ public class ReaderServiceTests { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("1").Build()) + .WithChapter(new ChapterBuilder("2").Build()) + .Build()) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) + .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List()), - EntityFactory.CreateChapter("2", false, new List()), - }), - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("A.cbz", true, new List()), - EntityFactory.CreateChapter("B.cbz", true, new List()), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -906,32 +917,33 @@ public class ReaderServiceTests // V1 -> V2 await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("1").Build()) + .WithChapter(new ChapterBuilder("2").Build()) + .Build()) + + .WithVolume(new VolumeBuilder("2") + .WithNumber(2) + .WithChapter(new ChapterBuilder("21").Build()) + .WithChapter(new ChapterBuilder("22").Build()) + .Build()) + + .WithVolume(new VolumeBuilder("3") + .WithNumber(3) + .WithChapter(new ChapterBuilder("31").Build()) + .WithChapter(new ChapterBuilder("32").Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List()), - EntityFactory.CreateChapter("2", false, new List()), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("21", false, new List()), - EntityFactory.CreateChapter("22", false, new List()), - }), - EntityFactory.CreateVolume("3", new List() - { - EntityFactory.CreateChapter("31", false, new List()), - EntityFactory.CreateChapter("32", false, new List()), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -953,33 +965,32 @@ public class ReaderServiceTests // V1 -> V2 await ResetDb(); - _context.Series.Add(new Series() - { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List()), - EntityFactory.CreateChapter("2", false, new List()), - }), - EntityFactory.CreateVolume("1.5", new List() - { - EntityFactory.CreateChapter("21", false, new List()), - EntityFactory.CreateChapter("22", false, new List()), - }), - EntityFactory.CreateVolume("3", new List() - { - EntityFactory.CreateChapter("31", false, new List()), - EntityFactory.CreateChapter("32", false, new List()), - }), - } - }); + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("1").Build()) + .WithChapter(new ChapterBuilder("2").Build()) + .Build()) + .WithVolume(new VolumeBuilder("1.5") + .WithNumber(2) + .WithChapter(new ChapterBuilder("21").Build()) + .WithChapter(new ChapterBuilder("22").Build()) + .Build()) + + .WithVolume(new VolumeBuilder("3") + .WithNumber(3) + .WithChapter(new ChapterBuilder("31").Build()) + .WithChapter(new ChapterBuilder("32").Build()) + .Build()) + .Build(); + series.Library = new Library() + { + Name = "Test LIb", + Type = LibraryType.Manga, + }; + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { UserName = "majora2007" @@ -999,39 +1010,34 @@ public class ReaderServiceTests { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("40").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("50").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("60").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("Some Special Title").WithPages(1).WithIsSpecial(true).Build()) + .Build()) + + .WithVolume(new VolumeBuilder("1997") + .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .Build()) + + .WithVolume(new VolumeBuilder("2001") + .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) + .Build()) + + .WithVolume(new VolumeBuilder("2005") + .WithChapter(new ChapterBuilder("31").WithPages(1).Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("40", false, new List(), 1), - EntityFactory.CreateChapter("50", false, new List(), 1), - EntityFactory.CreateChapter("60", false, new List(), 1), - EntityFactory.CreateChapter("Some Special Title", true, new List(), 1), - }), - EntityFactory.CreateVolume("1997", new List() - { - EntityFactory.CreateChapter("1", false, new List(), 1), - }), - EntityFactory.CreateVolume("2001", new List() - { - EntityFactory.CreateChapter("21", false, new List(), 1), - }), - EntityFactory.CreateVolume("2005", new List() - { - EntityFactory.CreateChapter("31", false, new List(), 1), - }), - } - }); - - - _context.AppUser.Add(new AppUser() + Name = "Test LIb", + Type = LibraryType.Manga, + }; + _context.Series.Add(series); + _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); @@ -1053,32 +1059,30 @@ public class ReaderServiceTests { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) + .Build()) + + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("22").WithPages(1).Build()) + .Build()) + + .WithVolume(new VolumeBuilder("3") + .WithChapter(new ChapterBuilder("31").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("32").WithPages(1).Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List()), - EntityFactory.CreateChapter("2", false, new List()), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("21", false, new List()), - EntityFactory.CreateChapter("22", false, new List()), - }), - EntityFactory.CreateVolume("3", new List() - { - EntityFactory.CreateChapter("31", false, new List()), - EntityFactory.CreateChapter("32", false, new List()), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -1100,27 +1104,27 @@ public class ReaderServiceTests { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("1").Build()) + .WithChapter(new ChapterBuilder("2").Build()) + .Build()) + + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) + .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List()), - EntityFactory.CreateChapter("2", false, new List()), - }), - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("A.cbz", true, new List()), - EntityFactory.CreateChapter("B.cbz", true, new List()), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -1143,22 +1147,21 @@ public class ReaderServiceTests { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("1").Build()) + .WithChapter(new ChapterBuilder("2").Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List()), - EntityFactory.CreateChapter("2", false, new List()), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -1179,21 +1182,20 @@ public class ReaderServiceTests { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("0").Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("0", false, new List()), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -1214,26 +1216,26 @@ public class ReaderServiceTests { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("1").Build()) + .WithChapter(new ChapterBuilder("2").Build()) + .Build()) + + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("0").Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("1", false, new List()), - EntityFactory.CreateChapter("2", false, new List()), - }), - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("0", false, new List()), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -1254,34 +1256,35 @@ public class ReaderServiceTests { await ResetDb(); - _context.Series.Add(new Series() - { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("5", false, new List()), - EntityFactory.CreateChapter("6", false, new List()), - EntityFactory.CreateChapter("7", false, new List()), + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("5").Build()) + .WithChapter(new ChapterBuilder("6").Build()) + .WithChapter(new ChapterBuilder("7").Build()) + .Build()) + + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("1").WithIsSpecial(true).Build()) + .WithChapter(new ChapterBuilder("2").WithIsSpecial(true).Build()) + .Build()) + + .WithVolume(new VolumeBuilder("2") + .WithNumber(2) + .WithChapter(new ChapterBuilder("3").WithIsSpecial(true).Build()) + .WithChapter(new ChapterBuilder("4").WithIsSpecial(true).Build()) + .Build()) + .Build(); + series.Library = new Library() + { + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); - }), - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List()), - EntityFactory.CreateChapter("2", false, new List()), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("3", false, new List()), - EntityFactory.CreateChapter("4", false, new List()), - }), - } - }); _context.AppUser.Add(new AppUser() { @@ -1306,22 +1309,21 @@ public class ReaderServiceTests { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("1").Build()) + .WithChapter(new ChapterBuilder("2").Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("1", false, new List()), - EntityFactory.CreateChapter("2", false, new List()), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -1342,27 +1344,28 @@ public class ReaderServiceTests { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("1").Build()) + .WithChapter(new ChapterBuilder("2").Build()) + .Build()) + + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) + .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List()), - EntityFactory.CreateChapter("2", false, new List()), - }), - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("A.cbz", true, new List()), - EntityFactory.CreateChapter("B.cbz", true, new List()), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); + _context.AppUser.Add(new AppUser() { @@ -1385,27 +1388,28 @@ public class ReaderServiceTests { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithNumber(0) + .WithChapter(new ChapterBuilder("1").Build()) + .WithChapter(new ChapterBuilder("2").Build()) + .Build()) + + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("21").Build()) + .WithChapter(new ChapterBuilder("22").Build()) + .Build()) + + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("1", false, new List()), - EntityFactory.CreateChapter("2", false, new List()), - }), - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("21", false, new List()), - EntityFactory.CreateChapter("22", false, new List()), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -1430,37 +1434,35 @@ public class ReaderServiceTests public async Task GetContinuePoint_ShouldReturnFirstVolume_NoProgress() { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("95").Build()) + .WithChapter(new ChapterBuilder("96").Build()) + .Build()) + + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1").Build()) + .WithChapter(new ChapterBuilder("2").Build()) + .Build()) + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("21").Build()) + .WithChapter(new ChapterBuilder("22").Build()) + .Build()) + .WithVolume(new VolumeBuilder("3") + .WithChapter(new ChapterBuilder("31").Build()) + .WithChapter(new ChapterBuilder("32").Build()) + .Build()) + + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("95", false, new List(), 1), - EntityFactory.CreateChapter("96", false, new List(), 1), - }), - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List(), 1), - EntityFactory.CreateChapter("2", false, new List(), 1), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("21", false, new List(), 1), - EntityFactory.CreateChapter("22", false, new List(), 1), - }), - EntityFactory.CreateVolume("3", new List() - { - EntityFactory.CreateChapter("31", false, new List(), 1), - EntityFactory.CreateChapter("32", false, new List(), 1), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); + _context.AppUser.Add(new AppUser() { @@ -1481,25 +1483,24 @@ public class ReaderServiceTests public async Task GetContinuePoint_ShouldReturnFirstVolume_WhenFirstVolumeIsAlsoTaggedAsChapter1_WithProgress() { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1").WithPages(3).Build()) + .Build()) + + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) + .Build()) + .WithPages(4) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List(), 3), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("0", false, new List(), 1), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -1527,32 +1528,29 @@ public class ReaderServiceTests public async Task GetContinuePoint_ShouldReturnFirstNonSpecial() { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) + .Build()) + + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("22").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("3") + .WithChapter(new ChapterBuilder("31").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("32").WithPages(1).Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List(), 1), - EntityFactory.CreateChapter("2", false, new List(), 1), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("21", false, new List(), 1), - EntityFactory.CreateChapter("22", false, new List(), 1), - }), - EntityFactory.CreateVolume("3", new List() - { - EntityFactory.CreateChapter("31", false, new List(), 1), - EntityFactory.CreateChapter("32", false, new List(), 1), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -1600,44 +1598,38 @@ public class ReaderServiceTests public async Task GetContinuePoint_ShouldReturnFirstNonSpecial2() { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + // Loose chapters + .WithVolume(new VolumeBuilder("0") + .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()) + .WithChapter(new ChapterBuilder("Some Special Title").WithIsSpecial(true).WithPages(1).Build()) + .Build()) + // One file volume + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) // Read + .Build()) + // Chapter-based volume + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) // Read + .WithChapter(new ChapterBuilder("22").WithPages(1).Build()) + .Build()) + // Chapter-based volume + .WithVolume(new VolumeBuilder("3") + .WithChapter(new ChapterBuilder("31").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("32").WithPages(1).Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - // Loose chapters - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("45", false, new List(), 1), - EntityFactory.CreateChapter("46", false, new List(), 1), - EntityFactory.CreateChapter("47", false, new List(), 1), - EntityFactory.CreateChapter("48", false, new List(), 1), - EntityFactory.CreateChapter("Some Special Title", true, new List(), 1), - }), + Name = "Test LIb", + Type = LibraryType.Manga, + }; - // One file volume - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("0", false, new List(), 1), // Read - }), - // Chapter-based volume - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("21", false, new List(), 1), // Read - EntityFactory.CreateChapter("22", false, new List(), 1), - }), - // Chapter-based volume - EntityFactory.CreateVolume("3", new List() - { - EntityFactory.CreateChapter("31", false, new List(), 1), - EntityFactory.CreateChapter("32", false, new List(), 1), - }), - } - }); + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -1679,31 +1671,28 @@ public class ReaderServiceTests public async Task GetContinuePoint_ShouldReturnFirstSpecial() { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) + .Build()) + + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("31").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("32").WithPages(1).Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List(), 1), - EntityFactory.CreateChapter("2", false, new List(), 1), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("21", false, new List(), 1), - }), - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("31", false, new List(), 1), - EntityFactory.CreateChapter("32", false, new List(), 1), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -1749,31 +1738,29 @@ public class ReaderServiceTests public async Task GetContinuePoint_ShouldReturnFirstChapter_WhenNonRead_LooseLeafChaptersAndVolumes() { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("230").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("231").WithPages(1).Build()) + .Build()) + + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("230", false, new List(), 1), - EntityFactory.CreateChapter("231", false, new List(), 1), - }), - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List(), 1), - EntityFactory.CreateChapter("2", false, new List(), 1), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("21", false, new List(), 1), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); + _context.AppUser.Add(new AppUser() { @@ -1792,32 +1779,29 @@ public class ReaderServiceTests public async Task GetContinuePoint_ShouldReturnLooseChapter_WhenAllVolumesAndAFewLooseChaptersRead() { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("100").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("101").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("102").WithPages(1).Build()) + .Build()) + + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("100", false, new List(), 1), - EntityFactory.CreateChapter("101", false, new List(), 1), - EntityFactory.CreateChapter("102", false, new List(), 1), - }), - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List(), 1), - EntityFactory.CreateChapter("2", false, new List(), 1), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("21", false, new List(), 1), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); var user = new AppUser() { @@ -1859,26 +1843,24 @@ public class ReaderServiceTests public async Task GetContinuePoint_ShouldReturnFirstChapter_WhenAllRead() { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) + .Build()) + + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List(), 1), - EntityFactory.CreateChapter("2", false, new List(), 1), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("21", false, new List(), 1), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -1923,28 +1905,26 @@ public class ReaderServiceTests public async Task GetContinuePoint_ShouldReturnFirstChapter_WhenAllReadAndAllChapters() { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .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("1") + .WithChapter(new ChapterBuilder("11").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("22").WithPages(1).Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("1", false, new List(), 1), - EntityFactory.CreateChapter("2", false, new List(), 1), - EntityFactory.CreateChapter("3", false, new List(), 1), - }), - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("11", false, new List(), 1), - EntityFactory.CreateChapter("22", false, new List(), 1), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -1969,24 +1949,22 @@ public class ReaderServiceTests public async Task GetContinuePoint_ShouldReturnFirstSpecial_WhenAllReadAndAllChapters() { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("3").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("Some Special Title").WithIsSpecial(true).WithPages(1).Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("1", false, new List(), 1), - EntityFactory.CreateChapter("2", false, new List(), 1), - EntityFactory.CreateChapter("3", false, new List(), 1), - EntityFactory.CreateChapter("Some Special Title", true, new List(), 1), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -2031,33 +2009,28 @@ public class ReaderServiceTests public async Task GetContinuePoint_ShouldReturnFirstVolumeChapter_WhenPreExistingProgress() { await ResetDb(); - var series = new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("230").WithPages(1).Build()) + //.WithChapter(new ChapterBuilder("231").WithPages(1).Build()) (Added later) + .Build()) + + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) + //.WithChapter(new ChapterBuilder("14.9").WithPages(1).Build()) (added later) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() - { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("230", false, new List(), 1), - //EntityFactory.CreateChapter("231", false, new List(), 1), (added later) - }), - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List(), 1), - EntityFactory.CreateChapter("2", false, new List(), 1), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("0", false, new List(), 1), - //EntityFactory.CreateChapter("14.9", false, new List(), 1), (added later) - }), - } + Name = "Test LIb", + Type = LibraryType.Manga, }; + _context.Series.Add(series); _context.AppUser.Add(new AppUser() @@ -2088,50 +2061,40 @@ public class ReaderServiceTests public async Task GetContinuePoint_ShouldReturnUnreadSingleVolume_WhenThereAreSomeSingleVolumesBeforeLooseLeafChapters() { await ResetDb(); - var readChapter1 = EntityFactory.CreateChapter("0", false, new List(), 1); - var readChapter2 = EntityFactory.CreateChapter("0", false, new List(), 1); + var readChapter1 = new ChapterBuilder("0").WithPages(1).Build(); + var readChapter2 = new ChapterBuilder("0").WithPages(1).Build(); + var volume = new VolumeBuilder("3").WithChapter(new ChapterBuilder("0").WithPages(1).Build()).Build(); - var volume = EntityFactory.CreateVolume("3", new List() - { - EntityFactory.CreateChapter("0", false, new List(), 1), - }); + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("51").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("52").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("53").WithPages(1).Build()) + .Build()) - _context.Series.Add(new Series() + .WithVolume(new VolumeBuilder("1") + .WithChapter(readChapter1) + .Build()) + .WithVolume(new VolumeBuilder("2") + .WithChapter(readChapter2) + .Build()) + // 3, 4, and all loose leafs are unread should be unread + .WithVolume(new VolumeBuilder("3") + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("4") + .WithChapter(new ChapterBuilder("40").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("41").WithPages(1).Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("51", false, new List(), 1), - EntityFactory.CreateChapter("52", false, new List(), 1), - EntityFactory.CreateChapter("53", false, new List(), 1), - }), - EntityFactory.CreateVolume("1", new List() - { - readChapter1 - }), - EntityFactory.CreateVolume("2", new List() - { - readChapter2 - }), - volume, - // 3, 4, and all loose leafs are unread should be unread - EntityFactory.CreateVolume("3", new List() - { - EntityFactory.CreateChapter("0", false, new List(), 1), - }), - EntityFactory.CreateVolume("4", new List() - { - EntityFactory.CreateChapter("40", false, new List(), 1), - EntityFactory.CreateChapter("41", false, new List(), 1), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() @@ -2165,24 +2128,22 @@ public class ReaderServiceTests public async Task MarkChaptersUntilAsRead_ShouldMarkAllChaptersAsRead() { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("3").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("Some Special Title").WithIsSpecial(true).WithPages(1).Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("1", false, new List(), 1), - EntityFactory.CreateChapter("2", false, new List(), 1), - EntityFactory.CreateChapter("3", false, new List(), 1), - EntityFactory.CreateChapter("Some Special Title", true, new List(), 1), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -2208,25 +2169,23 @@ public class ReaderServiceTests public async Task MarkChaptersUntilAsRead_ShouldMarkUptTillChapterNumberAsRead() { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .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()) + .WithChapter(new ChapterBuilder("Some Special Title").WithIsSpecial(true).WithPages(1).Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("1", false, new List(), 1), - EntityFactory.CreateChapter("2", false, new List(), 1), - EntityFactory.CreateChapter("2.5", false, new List(), 1), - EntityFactory.CreateChapter("3", false, new List(), 1), - EntityFactory.CreateChapter("Some Special Title", true, new List(), 1), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -2253,25 +2212,22 @@ public class ReaderServiceTests public async Task MarkChaptersUntilAsRead_ShouldMarkAsRead_OnlyVolumesWithChapter0() { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("0", false, new List(), 1), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("0", false, new List(), 1), - }), - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -2294,43 +2250,37 @@ public class ReaderServiceTests public async Task MarkChaptersUntilAsRead_ShouldMarkAsReadAnythingUntil() { await ResetDb(); - _context.Series.Add(new Series() - { - Name = "Test", - Library = new Library() - { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("45", false, new List(), 5), + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("45").WithPages(5).Build()) + .WithChapter(new ChapterBuilder("46").WithPages(46).Build()) + .WithChapter(new ChapterBuilder("47").WithPages(47).Build()) + .WithChapter(new ChapterBuilder("48").WithPages(48).Build()) + .WithChapter(new ChapterBuilder("49").WithPages(49).Build()) + .WithChapter(new ChapterBuilder("50").WithPages(50).Build()) + .WithChapter(new ChapterBuilder("Some Special Title").WithIsSpecial(true).WithPages(10).Build()) + .Build()) - EntityFactory.CreateChapter("46", false, new List(), 46), - EntityFactory.CreateChapter("47", false, new List(), 47), - EntityFactory.CreateChapter("48", false, new List(), 48), - EntityFactory.CreateChapter("49", false, new List(), 49), - EntityFactory.CreateChapter("50", false, new List(), 50), - EntityFactory.CreateChapter("Some Special Title", true, new List(), 10), - }), - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("0", false, new List(), 6), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("0", false, new List(), 7), - }), - EntityFactory.CreateVolume("3", new List() - { - EntityFactory.CreateChapter("12", false, new List(), 5), - EntityFactory.CreateChapter("13", false, new List(), 5), - EntityFactory.CreateChapter("14", false, new List(), 5), - }), - } - }); + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("0").WithPages(6).Build()) + .Build()) + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("0").WithPages(7).Build()) + .Build()) + .WithVolume(new VolumeBuilder("3") + .WithChapter(new ChapterBuilder("12").WithPages(5).Build()) + .WithChapter(new ChapterBuilder("13").WithPages(5).Build()) + .WithChapter(new ChapterBuilder("14").WithPages(5).Build()) + .Build()) + .Build(); + series.Library = new Library() + { + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -2370,46 +2320,25 @@ public class ReaderServiceTests public async Task MarkSeriesAsReadTest() { await ResetDb(); - - _context.Series.Add(new Series() + // TODO: Validate this is correct, shouldn't be possible to have 2 Volume 0's in a series + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("1").WithPages(2).Build()) + .Build()) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("1").WithPages(2).Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - new Volume() - { - Chapters = new List() - { - new Chapter() - { - Pages = 1 - }, - new Chapter() - { - Pages = 2 - } - } - }, - new Volume() - { - Chapters = new List() - { - new Chapter() - { - Pages = 1 - }, - new Chapter() - { - Pages = 2 - } - } - } - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -2435,32 +2364,20 @@ public class ReaderServiceTests public async Task MarkSeriesAsUnreadTest() { await ResetDb(); - - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("1").WithPages(2).Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - new Volume() - { - Chapters = new List() - { - new Chapter() - { - Pages = 1 - }, - new Chapter() - { - Pages = 2 - } - } - } - } - }); + Name = "Test LIb", + Type = LibraryType.Manga, + }; + + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -2524,38 +2441,33 @@ public class ReaderServiceTests public async Task MarkVolumesUntilAsRead_ShouldMarkVolumesAsRead() { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("10").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("20").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("30").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("Some Special Title").WithIsSpecial(true).WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("1997") + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) + .Build()) + + .WithVolume(new VolumeBuilder("2002") + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) + .Build()) + + .WithVolume(new VolumeBuilder("2003") + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() + Name = "Test LIb", + Type = LibraryType.Manga, + }; - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("10", false, new List(), 1), - EntityFactory.CreateChapter("20", false, new List(), 1), - EntityFactory.CreateChapter("30", false, new List(), 1), - EntityFactory.CreateChapter("Some Special Title", true, new List(), 1), - }), - - EntityFactory.CreateVolume("1997", new List() - { - EntityFactory.CreateChapter("0", false, new List(), 1), - }), - EntityFactory.CreateVolume("2002", new List() - { - EntityFactory.CreateChapter("0", false, new List(), 1), - }), - EntityFactory.CreateVolume("2003", new List() - { - EntityFactory.CreateChapter("0", false, new List(), 1), - }), - } - }); + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { @@ -2587,38 +2499,33 @@ public class ReaderServiceTests public async Task MarkVolumesUntilAsRead_ShouldMarkChapterBasedVolumesAsRead() { await ResetDb(); - _context.Series.Add(new Series() + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("10").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("20").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("30").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("Some Special Title").WithIsSpecial(true).WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("1997") + .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .Build()) + + .WithVolume(new VolumeBuilder("2002") + .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) + .Build()) + + .WithVolume(new VolumeBuilder("2003") + .WithChapter(new ChapterBuilder("3").WithPages(1).Build()) + .Build()) + .Build(); + series.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() + Name = "Test LIb", + Type = LibraryType.Manga, + }; - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("10", false, new List(), 1), - EntityFactory.CreateChapter("20", false, new List(), 1), - EntityFactory.CreateChapter("30", false, new List(), 1), - EntityFactory.CreateChapter("Some Special Title", true, new List(), 1), - }), - - EntityFactory.CreateVolume("1997", new List() - { - EntityFactory.CreateChapter("1", false, new List(), 1), - }), - EntityFactory.CreateVolume("2002", new List() - { - EntityFactory.CreateChapter("2", false, new List(), 1), - }), - EntityFactory.CreateVolume("2003", new List() - { - EntityFactory.CreateChapter("3", false, new List(), 1), - }), - } - }); + _context.Series.Add(series); _context.AppUser.Add(new AppUser() { diff --git a/API.Tests/Services/ReadingListServiceTests.cs b/API.Tests/Services/ReadingListServiceTests.cs index 7b036b7e5..a999c7e20 100644 --- a/API.Tests/Services/ReadingListServiceTests.cs +++ b/API.Tests/Services/ReadingListServiceTests.cs @@ -11,10 +11,12 @@ using API.DTOs.ReadingLists; using API.DTOs.ReadingLists.CBL; using API.Entities; using API.Entities.Enums; +using API.Extensions; using API.Helpers; using API.Services; using API.SignalR; using API.Tests.Helpers; +using API.Tests.Helpers.Builders; using AutoMapper; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; @@ -44,7 +46,7 @@ public class ReadingListServiceTests var config = new MapperConfiguration(cfg => cfg.AddProfile()); var mapper = config.CreateMapper(); - _unitOfWork = new UnitOfWork(_context, mapper, null); + _unitOfWork = new UnitOfWork(_context, mapper, null!); _readingListService = new ReadingListService(_unitOfWork, Substitute.For>(), Substitute.For()); } @@ -124,36 +126,26 @@ public class ReadingListServiceTests Type = LibraryType.Book, Series = new List() { - new Series() - { - Name = "Test", - Metadata = DbFactory.SeriesMetadata(new List()), - Volumes = new List() + new SeriesBuilder("Test") + .WithMetadata(DbFactory.SeriesMetadata(new List())) + .WithVolumes(new List() { - new Volume() - { - Name = "0", - Chapters = new List() - { - new Chapter() - { - Number = "1", - AgeRating = AgeRating.Everyone, - }, - new Chapter() - { - Number = "2", - AgeRating = AgeRating.X18Plus - }, - new Chapter() - { - Number = "3", - AgeRating = AgeRating.X18Plus - } - } - } - } - } + new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("1") + .WithAgeRating(AgeRating.Everyone) + .Build() + ) + .WithChapter(new ChapterBuilder("2") + .WithAgeRating(AgeRating.X18Plus) + .Build() + ) + .WithChapter(new ChapterBuilder("3") + .WithAgeRating(AgeRating.X18Plus) + .Build() + ) + .Build() + }) + .Build(), } }, } @@ -162,8 +154,8 @@ public class ReadingListServiceTests await _context.SaveChangesAsync(); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.ReadingLists); - var readingList = new ReadingList(); - user.ReadingLists = new List() + var readingList = DbFactory.ReadingList("test"); + user!.ReadingLists = new List() { readingList }; @@ -191,36 +183,26 @@ public class ReadingListServiceTests Type = LibraryType.Book, Series = new List() { - new Series() - { - Name = "Test", - Metadata = DbFactory.SeriesMetadata(new List()), - Volumes = new List() + new SeriesBuilder("Test") + .WithMetadata(DbFactory.SeriesMetadata(new List())) + .WithVolumes(new List() { - new Volume() - { - Name = "0", - Chapters = new List() - { - new Chapter() - { - Number = "1", - AgeRating = AgeRating.Everyone, - }, - new Chapter() - { - Number = "2", - AgeRating = AgeRating.X18Plus - }, - new Chapter() - { - Number = "3", - AgeRating = AgeRating.X18Plus - } - } - } - } - } + new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("1") + .WithAgeRating(AgeRating.Everyone) + .Build() + ) + .WithChapter(new ChapterBuilder("2") + .WithAgeRating(AgeRating.X18Plus) + .Build() + ) + .WithChapter(new ChapterBuilder("3") + .WithAgeRating(AgeRating.X18Plus) + .Build() + ) + .Build() + }) + .Build() } }, } @@ -229,8 +211,8 @@ public class ReadingListServiceTests await _context.SaveChangesAsync(); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.ReadingLists); - var readingList = new ReadingList(); - user.ReadingLists = new List() + var readingList = DbFactory.ReadingList("test"); + user!.ReadingLists = new List() { readingList }; @@ -265,36 +247,26 @@ public class ReadingListServiceTests Type = LibraryType.Book, Series = new List() { - new Series() - { - Name = "Test", - Metadata = DbFactory.SeriesMetadata(new List()), - Volumes = new List() + new SeriesBuilder("Test") + .WithMetadata(DbFactory.SeriesMetadata(new List())) + .WithVolumes(new List() { - new Volume() - { - Name = "0", - Chapters = new List() - { - new Chapter() - { - Number = "1", - AgeRating = AgeRating.Everyone, - }, - new Chapter() - { - Number = "2", - AgeRating = AgeRating.X18Plus - }, - new Chapter() - { - Number = "3", - AgeRating = AgeRating.X18Plus - } - } - } - } - } + new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("1") + .WithAgeRating(AgeRating.Everyone) + .Build() + ) + .WithChapter(new ChapterBuilder("2") + .WithAgeRating(AgeRating.X18Plus) + .Build() + ) + .WithChapter(new ChapterBuilder("3") + .WithAgeRating(AgeRating.X18Plus) + .Build() + ) + .Build() + }) + .Build() } }, } @@ -303,7 +275,7 @@ public class ReadingListServiceTests await _context.SaveChangesAsync(); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.ReadingLists); - var readingList = new ReadingList(); + var readingList = DbFactory.ReadingList("Test"); user.ReadingLists = new List() { readingList @@ -341,36 +313,26 @@ public class ReadingListServiceTests Type = LibraryType.Book, Series = new List() { - new Series() - { - Name = "Test", - Metadata = DbFactory.SeriesMetadata(new List()), - Volumes = new List() + new SeriesBuilder("Test") + .WithMetadata(DbFactory.SeriesMetadata(new List())) + .WithVolumes(new List() { - new Volume() - { - Name = "0", - Chapters = new List() - { - new Chapter() - { - Number = "1", - AgeRating = AgeRating.Everyone, - }, - new Chapter() - { - Number = "2", - AgeRating = AgeRating.X18Plus - }, - new Chapter() - { - Number = "3", - AgeRating = AgeRating.X18Plus - } - } - } - } - } + new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("1") + .WithAgeRating(AgeRating.Everyone) + .Build() + ) + .WithChapter(new ChapterBuilder("2") + .WithAgeRating(AgeRating.X18Plus) + .Build() + ) + .WithChapter(new ChapterBuilder("3") + .WithAgeRating(AgeRating.X18Plus) + .Build() + ) + .Build() + }) + .Build() } }, } @@ -379,8 +341,8 @@ public class ReadingListServiceTests await _context.SaveChangesAsync(); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.ReadingLists); - var readingList = new ReadingList(); - user.ReadingLists = new List() + var readingList = DbFactory.ReadingList("test"); + user!.ReadingLists = new List() { readingList }; @@ -436,31 +398,22 @@ public class ReadingListServiceTests Type = LibraryType.Book, Series = new List() { - new Series() - { - Name = "Test", - Metadata = DbFactory.SeriesMetadata(new List()), - Volumes = new List() + new SeriesBuilder("Test") + .WithMetadata(DbFactory.SeriesMetadata(new List())) + .WithVolumes(new List() { - new Volume() - { - Name = "0", - Chapters = new List() - { - new Chapter() - { - Number = "1", - AgeRating = AgeRating.Everyone - }, - new Chapter() - { - Number = "2", - AgeRating = AgeRating.X18Plus - } - } - } - } - } + new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("1") + .WithAgeRating(AgeRating.Everyone) + .Build() + ) + .WithChapter(new ChapterBuilder("2") + .WithAgeRating(AgeRating.X18Plus) + .Build() + ) + .Build() + }) + .Build(), } }, } @@ -469,7 +422,7 @@ public class ReadingListServiceTests await _context.SaveChangesAsync(); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.ReadingLists); - var readingList = new ReadingList(); + var readingList = DbFactory.ReadingList("Test"); user.ReadingLists = new List() { readingList @@ -508,39 +461,26 @@ public class ReadingListServiceTests Type = LibraryType.Book, Series = new List() { - new Series() - { - Name = "Test", - Metadata = DbFactory.SeriesMetadata(new List()), - Volumes = new List() + new SeriesBuilder("Test") + .WithMetadata(DbFactory.SeriesMetadata(new List())) + .WithVolumes(new List() { - new Volume() - { - Name = "0", - Chapters = new List() - { - new Chapter() - { - Number = "1", - AgeRating = AgeRating.Everyone, - Pages = 1 - }, - new Chapter() - { - Number = "2", - AgeRating = AgeRating.X18Plus, - Pages = 1 - }, - new Chapter() - { - Number = "3", - AgeRating = AgeRating.X18Plus, - Pages = 1 - } - } - } - } - } + new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("1") + .WithAgeRating(AgeRating.Everyone) + .Build() + ) + .WithChapter(new ChapterBuilder("2") + .WithAgeRating(AgeRating.X18Plus) + .Build() + ) + .WithChapter(new ChapterBuilder("3") + .WithAgeRating(AgeRating.X18Plus) + .Build() + ) + .Build() + }) + .Build() } }, } @@ -549,7 +489,7 @@ public class ReadingListServiceTests await _context.SaveChangesAsync(); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.ReadingLists | AppUserIncludes.Progress); - var readingList = new ReadingList(); + var readingList = DbFactory.ReadingList("Test"); user.ReadingLists = new List() { readingList @@ -594,29 +534,20 @@ public class ReadingListServiceTests Type = LibraryType.Book, Series = new List() { - new Series() - { - Name = "Test", - Metadata = DbFactory.SeriesMetadata(new List()), - Volumes = new List() + new SeriesBuilder("Test") + .WithMetadata(DbFactory.SeriesMetadata(new List())) + .WithVolumes(new List() { - new Volume() - { - Name = "0", - Chapters = new List() - { - new Chapter() - { - Number = "1", - }, - new Chapter() - { - Number = "2", - } - } - } - } - } + new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("1") + .Build() + ) + .WithChapter(new ChapterBuilder("2") + .Build() + ) + .Build() + }) + .Build() } }, } @@ -625,8 +556,8 @@ public class ReadingListServiceTests await _context.SaveChangesAsync(); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.ReadingLists); - var readingList = new ReadingList(); - user.ReadingLists = new List() + var readingList = DbFactory.ReadingList("Test"); + user!.ReadingLists = new List() { readingList }; @@ -645,29 +576,20 @@ public class ReadingListServiceTests public async Task CalculateAgeRating_ShouldUpdateToMax() { await ResetDb(); - var s = new Series() - { - Name = "Test", - Metadata = DbFactory.SeriesMetadata(new List()), - Volumes = new List() + var s = new SeriesBuilder("Test") + .WithMetadata(DbFactory.SeriesMetadata(new List())) + .WithVolumes(new List() { - new Volume() - { - Name = "0", - Chapters = new List() - { - new Chapter() - { - Number = "1", - }, - new Chapter() - { - Number = "2", - } - } - } - } - }; + new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("1") + .Build() + ) + .WithChapter(new ChapterBuilder("2") + .Build() + ) + .Build() + }) + .Build(); _context.AppUser.Add(new AppUser() { UserName = "majora2007", @@ -691,7 +613,7 @@ public class ReadingListServiceTests await _context.SaveChangesAsync(); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.ReadingLists); - var readingList = new ReadingList(); + var readingList = DbFactory.ReadingList("Test"); user.ReadingLists = new List() { readingList @@ -1108,33 +1030,53 @@ public class ReadingListServiceTests var cblReadingList = LoadCblFromPath("Fables.cbl"); // Mock up our series - var fablesSeries = DbFactory.Series("Fables"); - var fables2Series = DbFactory.Series("Fables: The Last Castle"); + // var fablesSeries = DbFactory.Series("Fables"); + // var fables2Series = DbFactory.Series("Fables: The Last Castle"); - fablesSeries.Volumes.Add(new Volume() - { - Number = 1, - Name = "2002", - Chapters = new List() - { - EntityFactory.CreateChapter("1", false), - EntityFactory.CreateChapter("2", false), - EntityFactory.CreateChapter("3", false), + var fablesSeries = new SeriesBuilder("Fables") + .WithVolume(new VolumeBuilder("2002") + .WithNumber(1) + .WithChapter(new ChapterBuilder("1").Build()) + .WithChapter(new ChapterBuilder("2").Build()) + .WithChapter(new ChapterBuilder("3").Build()) + .Build()) + .Build(); - } - }); - fables2Series.Volumes.Add(new Volume() - { - Number = 1, - Name = "2003", - Chapters = new List() - { - EntityFactory.CreateChapter("1", false), - EntityFactory.CreateChapter("2", false), - EntityFactory.CreateChapter("3", false), + var fables2Series = new SeriesBuilder("Fables: The Last Castle") + .WithVolume(new VolumeBuilder("2003") + .WithNumber(1) + .WithChapter(new ChapterBuilder("1").Build()) + .WithChapter(new ChapterBuilder("2").Build()) + .WithChapter(new ChapterBuilder("3").Build()) + .Build()) + .Build(); - } - }); + + + // fablesSeries.Volumes.Add(new Volume() + // { + // Number = 1, + // Name = "2002", + // Chapters = new List() + // { + // EntityFactory.CreateChapter("1", false), + // EntityFactory.CreateChapter("2", false), + // EntityFactory.CreateChapter("3", false), + // + // } + // }); + // fables2Series.Volumes.Add(new Volume() + // { + // Number = 1, + // Name = "2003", + // Chapters = new List() + // { + // EntityFactory.CreateChapter("1", false), + // EntityFactory.CreateChapter("2", false), + // EntityFactory.CreateChapter("3", false), + // + // } + // }); _context.AppUser.Add(new AppUser() { diff --git a/API.Tests/Services/ScannerServiceTests.cs b/API.Tests/Services/ScannerServiceTests.cs index 2298aa003..9f2073985 100644 --- a/API.Tests/Services/ScannerServiceTests.cs +++ b/API.Tests/Services/ScannerServiceTests.cs @@ -3,10 +3,12 @@ using System.Linq; using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; +using API.Extensions; using API.Parser; using API.Services.Tasks; using API.Services.Tasks.Scanner; using API.Tests.Helpers; +using API.Tests.Helpers.Builders; using Xunit; namespace API.Tests.Services; @@ -23,23 +25,14 @@ public class ScannerServiceTests var existingSeries = new List { - new Series() - { - Name = "Darker Than Black", - LocalizedName = "Darker Than Black", - OriginalName = "Darker Than Black", - Volumes = new List() - { - new Volume() - { - Number = 1, - Name = "1" - } - }, - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Darker Than Black"), - Metadata = new SeriesMetadata(), - Format = MangaFormat.Epub - } + new SeriesBuilder("Darker Than Black") + .WithFormat(MangaFormat.Epub) + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithName("1") + .Build()) + .WithLocalizedName("Darker Than Black") + .Build() }; Assert.Equal(1, ScannerService.FindSeriesNotOnDisk(existingSeries, infos).Count()); @@ -56,76 +49,42 @@ public class ScannerServiceTests var existingSeries = new List { - new Series() - { - Name = "Cage of Eden", - LocalizedName = "Cage of Eden", - OriginalName = "Cage of Eden", - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Cage of Eden"), - Metadata = new SeriesMetadata(), - Format = MangaFormat.Archive - }, - new Series() - { - Name = "Darker Than Black", - LocalizedName = "Darker Than Black", - OriginalName = "Darker Than Black", - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Darker Than Black"), - Metadata = new SeriesMetadata(), - Format = MangaFormat.Archive - } + new SeriesBuilder("Cage of Eden") + .WithFormat(MangaFormat.Archive) + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithName("1") + .Build()) + .WithLocalizedName("Darker Than Black") + .Build(), + new SeriesBuilder("Darker Than Black") + .WithFormat(MangaFormat.Archive) + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithName("1") + .Build()) + .WithLocalizedName("Darker Than Black") + .Build(), + // new Series() + // { + // Name = "Cage of Eden", + // LocalizedName = "Cage of Eden", + // OriginalName = "Cage of Eden", + // NormalizedName = "Darker Than Black".ToNormalized(), + // Metadata = new SeriesMetadata(), + // Format = MangaFormat.Archive + // }, + // new Series() + // { + // Name = "Darker Than Black", + // LocalizedName = "Darker Than Black", + // OriginalName = "Darker Than Black", + // NormalizedName = "Darker Than Black".ToNormalized(), + // Metadata = new SeriesMetadata(), + // Format = MangaFormat.Archive + // } }; - - Assert.Empty(ScannerService.FindSeriesNotOnDisk(existingSeries, infos)); } - - - // TODO: Figure out how to do this with ParseScannedFiles - // [Theory] - // [InlineData(new [] {"Darker than Black"}, "Darker than Black", "Darker than Black")] - // [InlineData(new [] {"Darker than Black"}, "Darker Than Black", "Darker than Black")] - // [InlineData(new [] {"Darker than Black"}, "Darker Than Black!", "Darker than Black")] - // [InlineData(new [] {""}, "Runaway Jack", "Runaway Jack")] - // public void MergeNameTest(string[] existingSeriesNames, string parsedInfoName, string expected) - // { - // var collectedSeries = new ConcurrentDictionary>(); - // foreach (var seriesName in existingSeriesNames) - // { - // AddToParsedInfo(collectedSeries, new ParserInfo() {Series = seriesName, Format = MangaFormat.Archive}); - // } - // - // var actualName = new ParseScannedFiles(_bookService, _logger).MergeName(collectedSeries, new ParserInfo() - // { - // Series = parsedInfoName, - // Format = MangaFormat.Archive - // }); - // - // Assert.Equal(expected, actualName); - // } - - // [Fact] - // public void RemoveMissingSeries_Should_RemoveSeries() - // { - // var existingSeries = new List() - // { - // EntityFactory.CreateSeries("Darker than Black Vol 1"), - // EntityFactory.CreateSeries("Darker than Black"), - // EntityFactory.CreateSeries("Beastars"), - // }; - // var missingSeries = new List() - // { - // EntityFactory.CreateSeries("Darker than Black Vol 1"), - // }; - // existingSeries = ScannerService.RemoveMissingSeries(existingSeries, missingSeries, out var removeCount).ToList(); - // - // Assert.DoesNotContain(missingSeries[0].Name, existingSeries.Select(s => s.Name)); - // Assert.Equal(missingSeries.Count, removeCount); - // } - - - // TODO: I want a test for UpdateSeries where if I have chapter 10 and now it's mapping into Vol 2 Chapter 10, - // if I can do it without deleting the underlying chapter (aka id change) - } diff --git a/API.Tests/Services/SeriesServiceTests.cs b/API.Tests/Services/SeriesServiceTests.cs index e0b24c812..20207a66f 100644 --- a/API.Tests/Services/SeriesServiceTests.cs +++ b/API.Tests/Services/SeriesServiceTests.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Data.Common; -using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; using API.Data; @@ -14,14 +12,10 @@ using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; using API.Extensions; -using API.Helpers; using API.Services; using API.SignalR; using API.Tests.Helpers; -using AutoMapper; -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; +using API.Tests.Helpers.Builders; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -93,28 +87,22 @@ public class SeriesServiceTests : AbstractDbTest Type = LibraryType.Book, Series = new List() { - new Series() - { - Name = "Test", - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("Omake", true, new List()), - EntityFactory.CreateChapter("Something SP02", true, new List()), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("21", false, new List()), - EntityFactory.CreateChapter("22", false, new List()), - }), - EntityFactory.CreateVolume("3", new List() - { - EntityFactory.CreateChapter("31", false, new List()), - EntityFactory.CreateChapter("32", false, new List()), - }), - } - } + new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("Omake").WithIsSpecial(true).WithTitle("Omake").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("Something SP02").WithIsSpecial(true).WithTitle("Something").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) + .Build()) + + .WithVolume(new VolumeBuilder("3") + .WithChapter(new ChapterBuilder("31").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("32").WithPages(1).Build()) + .Build()) + .Build(), } }); @@ -147,28 +135,22 @@ public class SeriesServiceTests : AbstractDbTest Type = LibraryType.Manga, Series = new List() { - new Series() - { - Name = "Test", - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("1", false, new List()), - EntityFactory.CreateChapter("2", false, new List()), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("21", false, new List()), - EntityFactory.CreateChapter("22", false, new List()), - }), - EntityFactory.CreateVolume("3", new List() - { - EntityFactory.CreateChapter("31", false, new List()), - EntityFactory.CreateChapter("32", false, new List()), - }), - } - } + new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) + .Build()) + + .WithVolume(new VolumeBuilder("3") + .WithChapter(new ChapterBuilder("31").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("32").WithPages(1).Build()) + .Build()) + .Build(), } }); @@ -201,26 +183,20 @@ public class SeriesServiceTests : AbstractDbTest Type = LibraryType.Manga, Series = new List() { - new Series() - { - Name = "Test", - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("1", false, new List()), - EntityFactory.CreateChapter("2", false, new List()), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("0", false, new List()), - }), - EntityFactory.CreateVolume("3", new List() - { - EntityFactory.CreateChapter("31", false, new List()), - }), - } - } + new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) + .Build()) + + .WithVolume(new VolumeBuilder("3") + .WithChapter(new ChapterBuilder("31").WithPages(1).Build()) + .Build()) + .Build(), } }); @@ -253,26 +229,20 @@ public class SeriesServiceTests : AbstractDbTest Type = LibraryType.Manga, Series = new List() { - new Series() - { - Name = "Test", - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("1", false, new List()), - EntityFactory.CreateChapter("2", false, new List()), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("0", false, new List()), - }), - EntityFactory.CreateVolume("3", new List() - { - EntityFactory.CreateChapter("31", false, new List()), - }), - } - } + new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) + .Build()) + + .WithVolume(new VolumeBuilder("3") + .WithChapter(new ChapterBuilder("31").WithPages(1).Build()) + .Build()) + .Build(), } }); @@ -308,21 +278,16 @@ public class SeriesServiceTests : AbstractDbTest Type = LibraryType.Book, Series = new List() { - new Series() - { - Name = "Test", - Volumes = new List() - { - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("0", false, new List()), - }), - EntityFactory.CreateVolume("3", new List() - { - EntityFactory.CreateChapter("0", false, new List()), - }), - } - } + new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) + .Build()) + + .WithVolume(new VolumeBuilder("3") + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) + .Build()) + .Build(), } }); @@ -354,21 +319,15 @@ public class SeriesServiceTests : AbstractDbTest Type = LibraryType.Book, Series = new List() { - new Series() - { - Name = "Test", - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("Ano Orokamono ni mo Kyakkou wo! - Volume 1.epub", true, new List()), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("Ano Orokamono ni mo Kyakkou wo! - Volume 2.epub", false, new List()), - }), - } - } + new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("Ano Orokamono ni mo Kyakkou wo! - Volume 1.epub", "Ano Orokamono ni mo Kyakkou wo! - Volume 1.epub").WithIsSpecial(true).WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("Ano Orokamono ni mo Kyakkou wo! - Volume 2.epub", "Ano Orokamono ni mo Kyakkou wo! - Volume 2.epub").WithPages(1).Build()) + .Build()) + .Build(), } }); @@ -407,25 +366,19 @@ public class SeriesServiceTests : AbstractDbTest Type = LibraryType.Manga, Series = new List() { - new Series() - { - Name = "Test", - Volumes = new List() - { - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("0", false, new List()), - }), - EntityFactory.CreateVolume("1.2", new List() - { - EntityFactory.CreateChapter("0", false, new List()), - }), - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("0", false, new List()), - }), - } - } + new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) + .Build()) + + .WithVolume(new VolumeBuilder("1.2") + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) + .Build()) + .Build(), } }); @@ -462,17 +415,12 @@ public class SeriesServiceTests : AbstractDbTest Type = LibraryType.Manga, Series = new List() { - new Series() - { - Name = "Test", - Volumes = new List() - { - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List(), 1), - }), - } - } + new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .Build()) + .Build(), } }); @@ -516,17 +464,12 @@ public class SeriesServiceTests : AbstractDbTest Type = LibraryType.Manga, Series = new List() { - new Series() - { - Name = "Test", - Volumes = new List() - { - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List(), 1), - }), - } - } + new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .Build()) + .Build(), } }); @@ -587,17 +530,12 @@ public class SeriesServiceTests : AbstractDbTest Type = LibraryType.Manga, Series = new List() { - new Series() - { - Name = "Test", - Volumes = new List() - { - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List(), 1), - }), - } - } + new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .Build()) + .Build(), } }); @@ -639,17 +577,12 @@ public class SeriesServiceTests : AbstractDbTest Type = LibraryType.Manga, Series = new List() { - new Series() - { - Name = "Test", - Volumes = new List() - { - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List(), 1), - }), - } - } + new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .Build()) + .Build(), } }); @@ -678,14 +611,16 @@ public class SeriesServiceTests : AbstractDbTest public async Task UpdateSeriesMetadata_ShouldCreateEmptyMetadata_IfDoesntExist() { await ResetDb(); - _context.Series.Add(new Series() + var s = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .Build(); + s.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Book, - } - }); + Name = "Test LIb", + Type = LibraryType.Book, + }; + + _context.Series.Add(s); await _context.SaveChangesAsync(); var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto() @@ -710,14 +645,16 @@ public class SeriesServiceTests : AbstractDbTest public async Task UpdateSeriesMetadata_ShouldCreateNewTags_IfNoneExist() { await ResetDb(); - _context.Series.Add(new Series() + var s = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .Build(); + s.Library = new Library() { - Name = "Test", - Library = new Library() { - Name = "Test LIb", - Type = LibraryType.Book, - } - }); + Name = "Test LIb", + Type = LibraryType.Book, + }; + + _context.Series.Add(s); await _context.SaveChangesAsync(); var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto() @@ -752,16 +689,15 @@ public class SeriesServiceTests : AbstractDbTest public async Task UpdateSeriesMetadata_ShouldRemoveExistingTags() { await ResetDb(); - var s = new Series() + var s = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadataBuilder().Build()) + .Build(); + s.Library = new Library() { - Name = "Test", - Library = new Library() - { - Name = "Test LIb", - Type = LibraryType.Book, - }, - Metadata = DbFactory.SeriesMetadata(new List()) + Name = "Test LIb", + Type = LibraryType.Book, }; + var g = DbFactory.Genre("Existing Genre"); s.Metadata.Genres = new List() {g}; _context.Series.Add(s); @@ -791,16 +727,15 @@ public class SeriesServiceTests : AbstractDbTest public async Task UpdateSeriesMetadata_ShouldAddNewPerson_NoExistingPeople() { await ResetDb(); - var s = new Series() + var s = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadataBuilder().Build()) + .Build(); + s.Library = new Library() { - Name = "Test", - Library = new Library() - { - Name = "Test LIb", - Type = LibraryType.Book, - }, - Metadata = DbFactory.SeriesMetadata(new List()) + Name = "Test LIb", + Type = LibraryType.Book, }; + var g = DbFactory.Person("Existing Person", PersonRole.Publisher); _context.Series.Add(s); @@ -829,15 +764,13 @@ public class SeriesServiceTests : AbstractDbTest public async Task UpdateSeriesMetadata_ShouldAddNewPerson_ExistingPeople() { await ResetDb(); - var s = new Series() + var s = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadataBuilder().Build()) + .Build(); + s.Library = new Library() { - Name = "Test", - Library = new Library() - { - Name = "Test LIb", - Type = LibraryType.Book, - }, - Metadata = DbFactory.SeriesMetadata(new List()) + Name = "Test LIb", + Type = LibraryType.Book, }; var g = DbFactory.Person("Existing Person", PersonRole.Publisher); s.Metadata.People = new List() {DbFactory.Person("Existing Writer", PersonRole.Writer), @@ -871,15 +804,13 @@ public class SeriesServiceTests : AbstractDbTest public async Task UpdateSeriesMetadata_ShouldRemoveExistingPerson() { await ResetDb(); - var s = new Series() + var s = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadataBuilder().Build()) + .Build(); + s.Library = new Library() { - Name = "Test", - Library = new Library() - { - Name = "Test LIb", - Type = LibraryType.Book, - }, - Metadata = DbFactory.SeriesMetadata(new List()) + Name = "Test LIb", + Type = LibraryType.Book, }; var g = DbFactory.Person("Existing Person", PersonRole.Publisher); _context.Series.Add(s); @@ -908,15 +839,13 @@ public class SeriesServiceTests : AbstractDbTest public async Task UpdateSeriesMetadata_ShouldLockIfTold() { await ResetDb(); - var s = new Series() + var s = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadataBuilder().Build()) + .Build(); + s.Library = new Library() { - Name = "Test", - Library = new Library() - { - Name = "Test LIb", - Type = LibraryType.Book, - }, - Metadata = DbFactory.SeriesMetadata(new List()) + Name = "Test LIb", + Type = LibraryType.Book, }; var g = DbFactory.Genre("Existing Genre"); s.Metadata.Genres = new List() {g}; @@ -949,15 +878,13 @@ public class SeriesServiceTests : AbstractDbTest public async Task UpdateSeriesMetadata_ShouldNotUpdateReleaseYear_IfLessThan1000() { await ResetDb(); - var s = new Series() + var s = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadataBuilder().Build()) + .Build(); + s.Library = new Library() { - Name = "Test", - Library = new Library() - { - Name = "Test LIb", - Type = LibraryType.Book, - }, - Metadata = DbFactory.SeriesMetadata(new List()) + Name = "Test LIb", + Type = LibraryType.Book, }; _context.Series.Add(s); await _context.SaveChangesAsync(); @@ -986,43 +913,37 @@ public class SeriesServiceTests : AbstractDbTest private static Series CreateSeriesMock() { - var files = new List() + var file = EntityFactory.CreateMangaFile("Test.cbz", MangaFormat.Archive, 1); + + var series = new SeriesBuilder("Test") + .WithMetadata(new SeriesMetadata()) + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("95").WithPages(1).WithFile(file).Build()) + .WithChapter(new ChapterBuilder("96").WithPages(1).WithFile(file).Build()) + .WithChapter(new ChapterBuilder("A Special Case").WithIsSpecial(true).WithFile(file).WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1").WithPages(1).WithFile(file).Build()) + .WithChapter(new ChapterBuilder("2").WithPages(1).WithFile(file).Build()) + .Build()) + + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("21").WithPages(1).WithFile(file).Build()) + .WithChapter(new ChapterBuilder("22").WithPages(1).WithFile(file).Build()) + .Build()) + + .WithVolume(new VolumeBuilder("3") + .WithChapter(new ChapterBuilder("31").WithPages(1).WithFile(file).Build()) + .WithChapter(new ChapterBuilder("32").WithPages(1).WithFile(file).Build()) + .Build()) + .Build(); + series.Library = new Library() { - EntityFactory.CreateMangaFile("Test.cbz", MangaFormat.Archive, 1) - }; - return new Series() - { - Name = "Test", - Library = new Library() - { - Name = "Test LIb", - Type = LibraryType.Manga, - }, - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("95", false, files, 1), - EntityFactory.CreateChapter("96", false, files, 1), - EntityFactory.CreateChapter("A Special Case", true, files, 1), - }), - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, files, 1), - EntityFactory.CreateChapter("2", false, files, 1), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("21", false, files, 1), - EntityFactory.CreateChapter("22", false, files, 1), - }), - EntityFactory.CreateVolume("3", new List() - { - EntityFactory.CreateChapter("31", false, files, 1), - EntityFactory.CreateChapter("32", false, files, 1), - }), - } + Name = "Test LIb", + Type = LibraryType.Manga, }; + + return series; } [Fact] @@ -1082,21 +1003,9 @@ public class SeriesServiceTests : AbstractDbTest Type = LibraryType.Book, Series = new List() { - new Series() - { - Name = "Test Series", - Volumes = new List(){} - }, - new Series() - { - Name = "Test Series Prequels", - Volumes = new List(){} - }, - new Series() - { - Name = "Test Series Sequels", - Volumes = new List(){} - } + new SeriesBuilder("Test Series").Build(), + new SeriesBuilder("Test Series Prequels").Build(), + new SeriesBuilder("Test Series Sequels").Build(), } }); @@ -1129,21 +1038,9 @@ public class SeriesServiceTests : AbstractDbTest Type = LibraryType.Book, Series = new List() { - new Series() - { - Name = "Test Series", - Volumes = new List(){} - }, - new Series() - { - Name = "Test Series Prequels", - Volumes = new List(){} - }, - new Series() - { - Name = "Test Series Sequels", - Volumes = new List(){} - } + DbFactory.Series("Test Series"), + DbFactory.Series("Test Series Prequels"), + DbFactory.Series("Test Series Sequels"), } }); @@ -1183,16 +1080,8 @@ public class SeriesServiceTests : AbstractDbTest Type = LibraryType.Book, Series = new List() { - new Series() - { - Name = "Series A", - Volumes = new List(){} - }, - new Series() - { - Name = "Series B", - Volumes = new List(){} - }, + DbFactory.Series("Series A"), + DbFactory.Series("Series B"), } }); @@ -1236,16 +1125,8 @@ public class SeriesServiceTests : AbstractDbTest Type = LibraryType.Book, Series = new List() { - new Series() - { - Name = "Series A", - Volumes = new List(){} - }, - new Series() - { - Name = "Series B", - Volumes = new List(){} - }, + DbFactory.Series("Series A"), + DbFactory.Series("Series B"), } }); @@ -1289,16 +1170,8 @@ public class SeriesServiceTests : AbstractDbTest Type = LibraryType.Book, Series = new List() { - new Series() - { - Name = "Test Series", - Volumes = new List(){} - }, - new Series() - { - Name = "Test Series Prequels", - Volumes = new List(){} - } + DbFactory.Series("Test Series"), + DbFactory.Series("Test Series Prequels"), } }); @@ -1342,31 +1215,11 @@ public class SeriesServiceTests : AbstractDbTest Type = LibraryType.Book, Series = new List() { - new Series() - { - Name = "Test Series", - Volumes = new List(){} - }, - new Series() - { - Name = "Test Series Editions", - Volumes = new List(){} - }, - new Series() - { - Name = "Test Series Prequels", - Volumes = new List(){} - }, - new Series() - { - Name = "Test Series Sequels", - Volumes = new List(){} - }, - new Series() - { - Name = "Test Series Adaption", - Volumes = new List(){} - } + DbFactory.Series("Test Series"), + DbFactory.Series("Test Series Editions"), + DbFactory.Series("Test Series Prequels"), + DbFactory.Series("Test Series Sequels"), + DbFactory.Series("Test Series Adaption"), } }); await _context.SaveChangesAsync(); @@ -1403,21 +1256,9 @@ public class SeriesServiceTests : AbstractDbTest Type = LibraryType.Book, Series = new List() { - new Series() - { - Name = "Test Series", - Volumes = new List() { } - }, - new Series() - { - Name = "Test Series Prequels", - Volumes = new List() { } - }, - new Series() - { - Name = "Test Series Sequels", - Volumes = new List() { } - } + DbFactory.Series("Test Series"), + DbFactory.Series("Test Series Prequels"), + DbFactory.Series("Test Series Sequels"), } }; _context.Library.Add(lib); @@ -1463,40 +1304,17 @@ public class SeriesServiceTests : AbstractDbTest Type = LibraryType.Book, Series = new List() { - new Series() - { - Name = "Test Series", - Volumes = new List() - { - new Volume() + new SeriesBuilder("Test Series") + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("1").WithFile(new MangaFile() { - Chapters = new List() - { - new Chapter() - { - Files = new List() - { - new MangaFile() - { - Pages = 1, - FilePath = "fake file" - } - } - } - } - } - } - }, - new Series() - { - Name = "Test Series Prequels", - Volumes = new List() { } - }, - new Series() - { - Name = "Test Series Sequels", - Volumes = new List() { } - } + Pages = 1, + FilePath = "fake file" + }).Build()) + .Build()) + .Build(), + new SeriesBuilder("Test Series Prequels").Build(), + new SeriesBuilder("Test Series Sequels").Build(), } }; _context.Library.Add(lib1); @@ -1513,21 +1331,9 @@ public class SeriesServiceTests : AbstractDbTest Type = LibraryType.Book, Series = new List() { - new Series() - { - Name = "Test Series 2", - Volumes = new List() { } - }, - new Series() - { - Name = "Test Series Prequels 2", - Volumes = new List() { } - }, - new Series() - { - Name = "Test Series Sequels 2", - Volumes = new List() { } - } + DbFactory.Series("Test Series 2"), + DbFactory.Series("Test Series Prequels 2"), + DbFactory.Series("Test Series Prequels 2"), } }; _context.Library.Add(lib2); diff --git a/API.Tests/Services/SiteThemeServiceTests.cs b/API.Tests/Services/SiteThemeServiceTests.cs index 2ab523e59..8bf32a0c1 100644 --- a/API.Tests/Services/SiteThemeServiceTests.cs +++ b/API.Tests/Services/SiteThemeServiceTests.cs @@ -1,138 +1,71 @@ -using System.Collections.Generic; -using System.Data.Common; -using System.IO.Abstractions.TestingHelpers; +using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; using API.Data; using API.Entities; -using API.Entities.Enums; using API.Entities.Enums.Theme; -using API.Entities.Enums.UserPreferences; -using API.Helpers; +using API.Extensions; using API.Services; using API.Services.Tasks; using API.SignalR; -using AutoMapper; using Kavita.Common; -using Microsoft.AspNetCore.SignalR; -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; +using Xunit.Abstractions; namespace API.Tests.Services; -public class SiteThemeServiceTests + +public abstract class SiteThemeServiceTest : AbstractDbTest { - private readonly ILogger _logger = Substitute.For>(); + private readonly ITestOutputHelper _testOutputHelper; private readonly IEventHub _messageHub = Substitute.For(); - private readonly DbConnection _connection; - private readonly DataContext _context; - private readonly IUnitOfWork _unitOfWork; - private const string CacheDirectory = "C:/kavita/config/cache/"; - private const string CoverImageDirectory = "C:/kavita/config/covers/"; - private const string BackupDirectory = "C:/kavita/config/backups/"; - private const string BookmarkDirectory = "C:/kavita/config/bookmarks/"; - private const string SiteThemeDirectory = "C:/kavita/config/themes/"; - - public SiteThemeServiceTests() + protected SiteThemeServiceTest(ITestOutputHelper testOutputHelper) : base() { - var contextOptions = new DbContextOptionsBuilder() - .UseSqlite(CreateInMemoryDatabase()) - .Options; - _connection = RelationalOptionsExtension.Extract(contextOptions).Connection; - - _context = new DataContext(contextOptions); - Task.Run(SeedDb).GetAwaiter().GetResult(); - - var config = new MapperConfiguration(cfg => cfg.AddProfile()); - var mapper = config.CreateMapper(); - _unitOfWork = new UnitOfWork(_context, mapper, null); + _testOutputHelper = testOutputHelper; } - #region Setup - - private static DbConnection CreateInMemoryDatabase() - { - var connection = new SqliteConnection("Filename=:memory:"); - - connection.Open(); - - return connection; - } - - private async Task SeedDb() - { - await _context.Database.MigrateAsync(); - var filesystem = CreateFileSystem(); - - await Seed.SeedSettings(_context, new DirectoryService(Substitute.For>(), filesystem)); - - var setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.CacheDirectory).SingleAsync(); - setting.Value = CacheDirectory; - - setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BackupDirectory).SingleAsync(); - setting.Value = BackupDirectory; - - setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BookmarkDirectory).SingleAsync(); - setting.Value = BookmarkDirectory; - - _context.ServerSetting.Update(setting); - - _context.AppUser.Add(new AppUser() - { - UserName = "Joe", - UserPreferences = new AppUserPreferences - { - Theme = Seed.DefaultThemes[0] - } - }); - - _context.Library.Add(new Library() - { - Name = "Manga", - Folders = new List() - { - new FolderPath() - { - Path = "C:/data/" - } - } - }); - return await _context.SaveChangesAsync() > 0; - } - - private static MockFileSystem CreateFileSystem() - { - var fileSystem = new MockFileSystem(); - fileSystem.Directory.SetCurrentDirectory("C:/kavita/"); - fileSystem.AddDirectory("C:/kavita/config/"); - fileSystem.AddDirectory(CacheDirectory); - fileSystem.AddDirectory(CoverImageDirectory); - fileSystem.AddDirectory(BackupDirectory); - fileSystem.AddDirectory(BookmarkDirectory); - fileSystem.AddDirectory(SiteThemeDirectory); - fileSystem.AddDirectory("C:/data/"); - - return fileSystem; - } - - private async Task ResetDb() + protected override async Task ResetDb() { _context.SiteTheme.RemoveRange(_context.SiteTheme); await _context.SaveChangesAsync(); + // Recreate defaults + await Seed.SeedThemes(_context); } - #endregion + [Fact] + public async Task UpdateDefault_ShouldThrowOnInvalidId() + { + await ResetDb(); + _testOutputHelper.WriteLine($"[UpdateDefault_ShouldThrowOnInvalidId] All Themes: {(await _unitOfWork.SiteThemeRepository.GetThemes()).Count(t => t.IsDefault)}"); + var filesystem = CreateFileSystem(); + filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123")); + var ds = new DirectoryService(Substitute.For>(), filesystem); + var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub); + + _context.SiteTheme.Add(new SiteTheme() + { + Name = "Custom", + NormalizedName = "Custom".ToNormalized(), + Provider = ThemeProvider.User, + FileName = "custom.css", + IsDefault = false + }); + await _context.SaveChangesAsync(); + + var ex = await Assert.ThrowsAsync(async () => await siteThemeService.UpdateDefault(10)); + Assert.Equal("Theme file missing or invalid", ex.Message); + + } [Fact] public async Task Scan_ShouldFindCustomFile() { await ResetDb(); + _testOutputHelper.WriteLine($"[Scan_ShouldOnlyInsertOnceOnSecondScan] All Themes: {(await _unitOfWork.SiteThemeRepository.GetThemes()).Count(t => t.IsDefault)}"); var filesystem = CreateFileSystem(); filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("")); var ds = new DirectoryService(Substitute.For>(), filesystem); @@ -146,6 +79,8 @@ public class SiteThemeServiceTests public async Task Scan_ShouldOnlyInsertOnceOnSecondScan() { await ResetDb(); + _testOutputHelper.WriteLine( + $"[Scan_ShouldOnlyInsertOnceOnSecondScan] All Themes: {(await _unitOfWork.SiteThemeRepository.GetThemes()).Count(t => t.IsDefault)}"); var filesystem = CreateFileSystem(); filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("")); var ds = new DirectoryService(Substitute.For>(), filesystem); @@ -157,7 +92,8 @@ public class SiteThemeServiceTests await siteThemeService.Scan(); var customThemes = (await _unitOfWork.SiteThemeRepository.GetThemeDtos()).Where(t => - API.Services.Tasks.Scanner.Parser.Parser.Normalize(t.Name).Equals(API.Services.Tasks.Scanner.Parser.Parser.Normalize("custom"))); + t.Name.ToNormalized().Equals("custom".ToNormalized())); + Assert.Single(customThemes); } @@ -165,6 +101,7 @@ public class SiteThemeServiceTests public async Task Scan_ShouldDeleteWhenFileDoesntExistOnSecondScan() { await ResetDb(); + _testOutputHelper.WriteLine($"[Scan_ShouldDeleteWhenFileDoesntExistOnSecondScan] All Themes: {(await _unitOfWork.SiteThemeRepository.GetThemes()).Count(t => t.IsDefault)}"); var filesystem = CreateFileSystem(); filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("")); var ds = new DirectoryService(Substitute.For>(), filesystem); @@ -176,16 +113,17 @@ public class SiteThemeServiceTests filesystem.RemoveFile($"{SiteThemeDirectory}custom.css"); await siteThemeService.Scan(); - var customThemes = (await _unitOfWork.SiteThemeRepository.GetThemeDtos()).Where(t => - API.Services.Tasks.Scanner.Parser.Parser.Normalize(t.Name).Equals(API.Services.Tasks.Scanner.Parser.Parser.Normalize("custom"))); + var themes = (await _unitOfWork.SiteThemeRepository.GetThemeDtos()); - Assert.Empty(customThemes); + Assert.Equal(0, themes.Count(t => + t.Name.ToNormalized().Equals("custom".ToNormalized()))); } [Fact] public async Task GetContent_ShouldReturnContent() { await ResetDb(); + _testOutputHelper.WriteLine($"[GetContent_ShouldReturnContent] All Themes: {(await _unitOfWork.SiteThemeRepository.GetThemes()).Count(t => t.IsDefault)}"); var filesystem = CreateFileSystem(); filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123")); var ds = new DirectoryService(Substitute.For>(), filesystem); @@ -194,7 +132,7 @@ public class SiteThemeServiceTests _context.SiteTheme.Add(new SiteTheme() { Name = "Custom", - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Custom"), + NormalizedName = "Custom".ToNormalized(), Provider = ThemeProvider.User, FileName = "custom.css", IsDefault = false @@ -211,6 +149,7 @@ public class SiteThemeServiceTests public async Task UpdateDefault_ShouldHaveOneDefault() { await ResetDb(); + _testOutputHelper.WriteLine($"[UpdateDefault_ShouldHaveOneDefault] All Themes: {(await _unitOfWork.SiteThemeRepository.GetThemes()).Count(t => t.IsDefault)}"); var filesystem = CreateFileSystem(); filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123")); var ds = new DirectoryService(Substitute.For>(), filesystem); @@ -219,7 +158,7 @@ public class SiteThemeServiceTests _context.SiteTheme.Add(new SiteTheme() { Name = "Custom", - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Custom"), + NormalizedName = "Custom".ToNormalized(), Provider = ThemeProvider.User, FileName = "custom.css", IsDefault = false @@ -228,6 +167,7 @@ public class SiteThemeServiceTests var customTheme = (await _unitOfWork.SiteThemeRepository.GetThemeDtoByName("Custom")); + Assert.NotNull(customTheme); await siteThemeService.UpdateDefault(customTheme.Id); @@ -235,31 +175,5 @@ public class SiteThemeServiceTests Assert.Equal(customTheme.Id, (await _unitOfWork.SiteThemeRepository.GetDefaultTheme()).Id); } - [Fact] - public async Task UpdateDefault_ShouldThrowOnInvalidId() - { - await ResetDb(); - var filesystem = CreateFileSystem(); - filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123")); - var ds = new DirectoryService(Substitute.For>(), filesystem); - var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub); - - _context.SiteTheme.Add(new SiteTheme() - { - Name = "Custom", - NormalizedName = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Custom"), - Provider = ThemeProvider.User, - FileName = "custom.css", - IsDefault = false - }); - await _context.SaveChangesAsync(); - - - - var ex = await Assert.ThrowsAsync(async () => await siteThemeService.UpdateDefault(10)); - Assert.Equal("Theme file missing or invalid", ex.Message); - - } - - } + diff --git a/API.Tests/Services/TachiyomiServiceTests.cs b/API.Tests/Services/TachiyomiServiceTests.cs index f623890d6..77971b444 100644 --- a/API.Tests/Services/TachiyomiServiceTests.cs +++ b/API.Tests/Services/TachiyomiServiceTests.cs @@ -1,4 +1,7 @@ -namespace API.Tests.Services; +using API.Extensions; +using API.Tests.Helpers.Builders; + +namespace API.Tests.Services; using System.Collections.Generic; using System.Data.Common; using System.IO.Abstractions.TestingHelpers; @@ -111,33 +114,25 @@ public class TachiyomiServiceTests { await ResetDb(); - var series = new Series - { - Name = "Test", - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("95", false, new List(), 1), - EntityFactory.CreateChapter("96", false, new List(), 1), - }), - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", true, new List(), 1), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("3", false, new List(), 1), - EntityFactory.CreateChapter("4", false, new List(), 1), - }), - EntityFactory.CreateVolume("3", new List() - { - EntityFactory.CreateChapter("31", false, new List(), 1), - EntityFactory.CreateChapter("32", false, new List(), 1), - }), - }, - Pages = 7 - }; + var series = new SeriesBuilder("Test") + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("3").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("4").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("3") + .WithChapter(new ChapterBuilder("31").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("32").WithPages(1).Build()) + .Build()) + .WithPages(7) + .Build(); + var library = new Library() { Name = "Test LIb", @@ -169,33 +164,25 @@ public class TachiyomiServiceTests { await ResetDb(); - var series = new Series - { - Name = "Test", - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("95", false, new List(), 1), - EntityFactory.CreateChapter("96", false, new List(), 1), - }), - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", true, new List(), 1), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("3", false, new List(), 1), - EntityFactory.CreateChapter("4", false, new List(), 1), - }), - EntityFactory.CreateVolume("3", new List() - { - EntityFactory.CreateChapter("31", false, new List(), 1), - EntityFactory.CreateChapter("32", false, new List(), 1), - }), - }, - Pages = 7 - }; + var series = new SeriesBuilder("Test") + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("3").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("4").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("3") + .WithChapter(new ChapterBuilder("31").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("32").WithPages(1).Build()) + .Build()) + .WithPages(7) + .Build(); + var library = new Library() { Name = "Test LIb", @@ -233,33 +220,25 @@ public class TachiyomiServiceTests { await ResetDb(); - var series = new Series - { - Name = "Test", - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("95", false, new List(), 1), - EntityFactory.CreateChapter("96", false, new List(), 1), - }), - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List(), 1), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("21", false, new List(), 1), - EntityFactory.CreateChapter("23", false, new List(), 1), - }), - EntityFactory.CreateVolume("3", new List() - { - EntityFactory.CreateChapter("31", false, new List(), 1), - EntityFactory.CreateChapter("32", false, new List(), 1), - }), - }, - Pages = 7 - }; + var series = new SeriesBuilder("Test") + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("22").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("3") + .WithChapter(new ChapterBuilder("31").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("32").WithPages(1).Build()) + .Build()) + .WithPages(7) + .Build(); + var library = new Library() { Name = "Test LIb", @@ -296,33 +275,25 @@ public class TachiyomiServiceTests { await ResetDb(); - var series = new Series - { - Name = "Test", - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("95", false, new List(), 1), - EntityFactory.CreateChapter("96", false, new List(), 1), - }), - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", true, new List(), 1), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("21", false, new List(), 1), - EntityFactory.CreateChapter("23", false, new List(), 1), - }), - EntityFactory.CreateVolume("3", new List() - { - EntityFactory.CreateChapter("31", false, new List(), 1), - EntityFactory.CreateChapter("32", false, new List(), 1), - }), - }, - Pages = 7 - }; + var series = new SeriesBuilder("Test") + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("22").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("3") + .WithChapter(new ChapterBuilder("31").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("32").WithPages(1).Build()) + .Build()) + .WithPages(7) + .Build(); + var library = new Library() { Name = "Test LIb", @@ -360,26 +331,19 @@ public class TachiyomiServiceTests { await ResetDb(); - var series = new Series - { - Name = "Test", - Volumes = new List() - { - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("0", false, new List(), 199), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("0", false, new List(), 192), - }), - EntityFactory.CreateVolume("3", new List() - { - EntityFactory.CreateChapter("0", false, new List(), 255), - }), - }, - Pages = 646 - }; + var series = new SeriesBuilder("Test") + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("0").WithPages(199).Build()) + .Build()) + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("0").WithPages(192).Build()) + .Build()) + .WithVolume(new VolumeBuilder("3") + .WithChapter(new ChapterBuilder("0").WithPages(255).Build()) + .Build()) + .WithPages(646) + .Build(); + var library = new Library() { Name = "Test LIb", @@ -418,31 +382,23 @@ public class TachiyomiServiceTests { await ResetDb(); - var series = new Series - { - Name = "Test", - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("95", false, new List(), 1), - EntityFactory.CreateChapter("96", false, new List(), 1), - }), - EntityFactory.CreateVolume("1997", new List() - { - EntityFactory.CreateChapter("1", false, new List(), 1), - }), - EntityFactory.CreateVolume("2002", new List() - { - EntityFactory.CreateChapter("2", false, new List(), 1), - }), - EntityFactory.CreateVolume("2005", new List() - { - EntityFactory.CreateChapter("3", false, new List(), 1), - }), - }, - Pages = 7 - }; + var series = new SeriesBuilder("Test") + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("1997") + .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("2002") + .WithChapter(new ChapterBuilder("2").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("2005") + .WithChapter(new ChapterBuilder("3").WithPages(1).Build()) + .Build()) + .WithPages(7) + .Build(); + var library = new Library() { Name = "Test LIb", @@ -485,33 +441,25 @@ public class TachiyomiServiceTests { await ResetDb(); - var series = new Series - { - Name = "Test", - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("95", false, new List(), 1), - EntityFactory.CreateChapter("96", false, new List(), 1), - }), - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", true, new List(), 1), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("3", false, new List(), 1), - EntityFactory.CreateChapter("4", false, new List(), 1), - }), - EntityFactory.CreateVolume("3", new List() - { - EntityFactory.CreateChapter("31", false, new List(), 1), - EntityFactory.CreateChapter("32", false, new List(), 1), - }), - }, - Pages = 7 - }; + var series = new SeriesBuilder("Test") + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("3").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("4").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("3") + .WithChapter(new ChapterBuilder("31").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("32").WithPages(1).Build()) + .Build()) + .WithPages(7) + .Build(); + var library = new Library() { Name = "Test LIb", @@ -542,33 +490,25 @@ public class TachiyomiServiceTests { await ResetDb(); - var series = new Series - { - Name = "Test", - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("95", false, new List(), 1), - EntityFactory.CreateChapter("96", false, new List(), 1), - }), - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", true, new List(), 1), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("3", false, new List(), 1), - EntityFactory.CreateChapter("4", false, new List(), 1), - }), - EntityFactory.CreateVolume("3", new List() - { - EntityFactory.CreateChapter("31", false, new List(), 1), - EntityFactory.CreateChapter("32", false, new List(), 1), - }), - }, - Pages = 7 - }; + var series = new SeriesBuilder("Test") + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("3").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("4").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("3") + .WithChapter(new ChapterBuilder("31").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("32").WithPages(1).Build()) + .Build()) + .WithPages(7) + .Build(); + var library = new Library() { Name = "Test LIb", @@ -606,33 +546,25 @@ public class TachiyomiServiceTests { await ResetDb(); - var series = new Series - { - Name = "Test", - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("95", false, new List(), 1), - EntityFactory.CreateChapter("96", false, new List(), 1), - }), - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", false, new List(), 1), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("21", false, new List(), 1), - EntityFactory.CreateChapter("23", false, new List(), 1), - }), - EntityFactory.CreateVolume("3", new List() - { - EntityFactory.CreateChapter("31", false, new List(), 1), - EntityFactory.CreateChapter("32", false, new List(), 1), - }), - }, - Pages = 7 - }; + var series = new SeriesBuilder("Test") + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("23").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("3") + .WithChapter(new ChapterBuilder("31").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("32").WithPages(1).Build()) + .Build()) + .WithPages(7) + .Build(); + var library = new Library() { Name = "Test LIb", @@ -668,34 +600,25 @@ public class TachiyomiServiceTests public async Task MarkChaptersUntilAsRead_ShouldReturnEncodedVolume_Progress() { await ResetDb(); + var series = new SeriesBuilder("Test") + .WithVolume(new VolumeBuilder("0") + .WithChapter(new ChapterBuilder("95").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("96").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("21").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("23").WithPages(1).Build()) + .Build()) + .WithVolume(new VolumeBuilder("3") + .WithChapter(new ChapterBuilder("31").WithPages(1).Build()) + .WithChapter(new ChapterBuilder("32").WithPages(1).Build()) + .Build()) + .WithPages(7) + .Build(); - var series = new Series - { - Name = "Test", - Volumes = new List() - { - EntityFactory.CreateVolume("0", new List() - { - EntityFactory.CreateChapter("95", false, new List(), 1), - EntityFactory.CreateChapter("96", false, new List(), 1), - }), - EntityFactory.CreateVolume("1", new List() - { - EntityFactory.CreateChapter("1", true, new List(), 1), - }), - EntityFactory.CreateVolume("2", new List() - { - EntityFactory.CreateChapter("21", false, new List(), 1), - EntityFactory.CreateChapter("23", false, new List(), 1), - }), - EntityFactory.CreateVolume("3", new List() - { - EntityFactory.CreateChapter("31", false, new List(), 1), - EntityFactory.CreateChapter("32", false, new List(), 1), - }), - }, - Pages = 7 - }; var library = new Library() { Name = "Test LIb", diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/sorting.expected.png b/API.Tests/Services/Test Data/ArchiveService/CoverImages/sorting.expected.png index 3ef55227f..74dc020e6 100644 Binary files a/API.Tests/Services/Test Data/ArchiveService/CoverImages/sorting.expected.png and b/API.Tests/Services/Test Data/ArchiveService/CoverImages/sorting.expected.png differ diff --git a/API.Tests/Services/WordCountAnalysisTests.cs b/API.Tests/Services/WordCountAnalysisTests.cs index 845f729de..c9e178665 100644 --- a/API.Tests/Services/WordCountAnalysisTests.cs +++ b/API.Tests/Services/WordCountAnalysisTests.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; -using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; using API.Entities; diff --git a/API/API.csproj b/API/API.csproj index 24df972e2..fcc2ae2ac 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -2,12 +2,14 @@ Default - net6.0 + net7.0 true Linux true true ../favicon.ico + warnings + latestmajor @@ -55,32 +57,32 @@ - + - - - + + + - + - - - + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + - - + + - + @@ -89,16 +91,16 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - - + + + diff --git a/API/Comparators/NumericComparer.cs b/API/Comparators/NumericComparer.cs index ae603e71b..194d013ea 100644 --- a/API/Comparators/NumericComparer.cs +++ b/API/Comparators/NumericComparer.cs @@ -5,7 +5,7 @@ namespace API.Comparators; public class NumericComparer : IComparer { - public int Compare(object x, object y) + public int Compare(object? x, object? y) { if((x is string xs) && (y is string ys)) { diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index a5815250c..7779a5f3d 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading.Tasks; -using System.Web; using API.Constants; using API.Data; using API.Data.Repositories; @@ -23,7 +22,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace API.Controllers; @@ -213,10 +211,13 @@ public class AccountController : BaseApiController var dto = _mapper.Map(user); dto.Token = await _tokenService.CreateToken(user); dto.RefreshToken = await _tokenService.CreateRefreshToken(user); - var pref = await _unitOfWork.UserRepository.GetPreferencesAsync(user.UserName); + var pref = await _unitOfWork.UserRepository.GetPreferencesAsync(user.UserName!); + if (pref == null) return Ok(dto); + pref.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme(); dto.Preferences = _mapper.Map(pref); - return dto; + + return Ok(dto); } /// @@ -248,7 +249,7 @@ public class AccountController : BaseApiController .GetFields(BindingFlags.Public | BindingFlags.Static) .Where(f => f.FieldType == typeof(string)) .ToDictionary(f => f.Name, - f => (string) f.GetValue(null)).Values.ToList(); + f => (string) f.GetValue(null)!).Values.ToList(); } @@ -260,6 +261,7 @@ public class AccountController : BaseApiController public async Task> ResetApiKey() { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); + if (user == null) return Unauthorized(); user.ApiKey = HashUtil.ApiKey(); @@ -281,7 +283,7 @@ public class AccountController : BaseApiController /// /// Returns just if the email was sent or server isn't reachable [HttpPost("update/email")] - public async Task UpdateEmail(UpdateEmailDto dto) + public async Task UpdateEmail(UpdateEmailDto? dto) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); if (user == null) return Unauthorized("You do not have permission"); @@ -297,7 +299,7 @@ public class AccountController : BaseApiController } // Validate no other users exist with this email - if (user.Email.Equals(dto.Email)) return Ok("Nothing to do"); + if (user.Email!.Equals(dto.Email)) return Ok("Nothing to do"); // Check if email is used by another user var existingUserEmail = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); @@ -335,7 +337,7 @@ public class AccountController : BaseApiController { EmailAddress = string.IsNullOrEmpty(user.Email) ? dto.Email : user.Email, InstallId = BuildInfo.Version.ToString(), - InvitingUser = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).First().UserName, + InvitingUser = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).First().UserName!, ServerConfirmationLink = emailLink }); } @@ -357,7 +359,7 @@ public class AccountController : BaseApiController } - 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(); } @@ -367,9 +369,9 @@ public class AccountController : BaseApiController { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); if (user == null) return Unauthorized("You do not have permission"); - if (dto == null) return BadRequest("Invalid payload"); var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user); + if (!await _accountService.HasChangeRestrictionRole(user)) return BadRequest("You do not have permission"); user.AgeRestriction = isAdmin ? AgeRating.NotApplicable : dto.AgeRating; user.AgeRestrictionIncludeUnknowns = isAdmin || dto.IncludeUnknowns; @@ -387,7 +389,7 @@ public class AccountController : BaseApiController return BadRequest("There was an error updating the age restriction"); } - 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(); } @@ -402,13 +404,14 @@ public class AccountController : BaseApiController public async Task UpdateAccount(UpdateUserDto dto) { var adminUser = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); + if (adminUser == null) return Unauthorized(); if (!await _unitOfWork.UserRepository.IsUserAdminAsync(adminUser)) return Unauthorized("You do not have permission"); var user = await _unitOfWork.UserRepository.GetUserByIdAsync(dto.UserId); if (user == null) return BadRequest("User does not exist"); // Check if username is changing - if (!user.UserName.Equals(dto.Username)) + if (!user.UserName!.Equals(dto.Username)) { // Validate username change var errors = await _accountService.ValidateUsername(dto.Username); @@ -488,12 +491,13 @@ public class AccountController : BaseApiController public async Task> GetInviteUrl(int userId, bool withBaseUrl) { var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + if (user == null) return Unauthorized(); if (user.EmailConfirmed) return BadRequest("User is already confirmed"); if (string.IsNullOrEmpty(user.ConfirmationToken)) return BadRequest("Manual setup is unable to be completed. Please cancel and recreate the invite."); - return await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", user.Email, withBaseUrl); + return await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", user.Email!, withBaseUrl); } @@ -520,23 +524,14 @@ public class AccountController : BaseApiController if (emailValidationErrors.Any()) { var invitedUser = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); - if (await _userManager.IsEmailConfirmedAsync(invitedUser)) - return BadRequest($"User is already registered as {invitedUser.UserName}"); + if (await _userManager.IsEmailConfirmedAsync(invitedUser!)) + return BadRequest($"User is already registered as {invitedUser!.UserName}"); return BadRequest("User is already invited under this email and has yet to accepted invite."); } } // Create a new user - var user = new AppUser() - { - UserName = dto.Email, - Email = dto.Email, - ApiKey = HashUtil.ApiKey(), - UserPreferences = new AppUserPreferences - { - Theme = await _unitOfWork.SiteThemeRepository.GetDefaultTheme() - } - }; + var user = DbFactory.AppUser(dto.Email, dto.Email, await _unitOfWork.SiteThemeRepository.GetDefaultTheme()); try { @@ -612,7 +607,7 @@ public class AccountController : BaseApiController await _emailService.SendConfirmationEmail(new ConfirmationEmailDto() { EmailAddress = dto.Email, - InvitingUser = adminUser.UserName, + InvitingUser = adminUser.UserName!, ServerConfirmationLink = emailLink }); } @@ -680,14 +675,14 @@ public class AccountController : BaseApiController await _unitOfWork.CommitAsync(); - user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(user.UserName, - AppUserIncludes.UserPreferences); + user = (await _unitOfWork.UserRepository.GetUserByUsernameAsync(user.UserName, + AppUserIncludes.UserPreferences))!; // Perform Login code return new UserDto { - Username = user.UserName, - Email = user.Email, + Username = user.UserName!, + Email = user.Email!, Token = await _tokenService.CreateToken(user), RefreshToken = await _tokenService.CreateRefreshToken(user), ApiKey = user.ApiKey, @@ -731,7 +726,7 @@ public class AccountController : BaseApiController // For the user's connected devices to pull the new information in await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, - MessageFactory.UserUpdateEvent(user.Id, user.UserName), user.Id); + MessageFactory.UserUpdateEvent(user.Id, user.UserName!), user.Id); // Perform Login code return Ok(); @@ -832,14 +827,14 @@ public class AccountController : BaseApiController await _unitOfWork.CommitAsync(); - user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(user.UserName, + user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(user.UserName!, AppUserIncludes.UserPreferences); // Perform Login code return new UserDto { - Username = user.UserName, - Email = user.Email, + Username = user!.UserName!, + Email = user.Email!, Token = await _tokenService.CreateToken(user), RefreshToken = await _tokenService.CreateRefreshToken(user), ApiKey = user.ApiKey, @@ -873,8 +868,8 @@ public class AccountController : BaseApiController { await _emailService.SendMigrationEmail(new EmailMigrationDto() { - EmailAddress = user.Email, - Username = user.UserName, + EmailAddress = user.Email!, + Username = user.UserName!, ServerConfirmationLink = emailLink, InstallId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value }); @@ -908,8 +903,8 @@ public class AccountController : BaseApiController if (emailValidationErrors.Any()) { var invitedUser = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); - if (await _userManager.IsEmailConfirmedAsync(invitedUser)) - return BadRequest($"User is already registered as {invitedUser.UserName}"); + if (await _userManager.IsEmailConfirmedAsync(invitedUser!)) + return BadRequest($"User is already registered as {invitedUser!.UserName}"); _logger.LogInformation("A user is attempting to login, but hasn't accepted email invite"); return BadRequest("User is already invited under this email and has yet to accepted invite."); diff --git a/API/Controllers/BookController.cs b/API/Controllers/BookController.cs index ea4ba9bdb..62cbcd436 100644 --- a/API/Controllers/BookController.cs +++ b/API/Controllers/BookController.cs @@ -37,6 +37,7 @@ public class BookController : BaseApiController public async Task> GetBookInfo(int chapterId) { var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId); + if (dto == null) return BadRequest("Chapter does not exist"); var bookTitle = string.Empty; switch (dto.SeriesFormat) { @@ -93,6 +94,7 @@ public class BookController : BaseApiController { if (chapterId <= 0) return BadRequest("Chapter is not valid"); var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId); + if (chapter == null) return BadRequest("Chapter is not valid"); using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.BookReaderOptions); var key = BookService.CoalesceKeyForAnyFile(book, file); @@ -116,8 +118,9 @@ public class BookController : BaseApiController public async Task>> GetBookChapters(int chapterId) { if (chapterId <= 0) return BadRequest("Chapter is not valid"); - var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId); + if (chapter == null) return BadRequest("Chapter is not valid"); + try { return Ok(await _bookService.GenerateTableOfContents(chapter)); @@ -140,6 +143,7 @@ public class BookController : BaseApiController public async Task> GetBookPage(int chapterId, [FromQuery] int page) { var chapter = await _cacheService.Ensure(chapterId); + if (chapter == null) return BadRequest("Could not find Chapter"); var path = _cacheService.GetCachedFile(chapter); var baseUrl = "//" + Request.Host + Request.PathBase + "/api/"; diff --git a/API/Controllers/CBLController.cs b/API/Controllers/CBLController.cs index f897866f1..6f46df2de 100644 --- a/API/Controllers/CBLController.cs +++ b/API/Controllers/CBLController.cs @@ -1,7 +1,5 @@ using System.IO; -using System.Linq; using System.Threading.Tasks; -using API.DTOs.ReadingLists; using API.DTOs.ReadingLists.CBL; using API.Extensions; using API.Services; diff --git a/API/Controllers/CollectionController.cs b/API/Controllers/CollectionController.cs index 0800f3977..e6dc91f5b 100644 --- a/API/Controllers/CollectionController.cs +++ b/API/Controllers/CollectionController.cs @@ -1,14 +1,12 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using API.Data; +using API.Data.Repositories; using API.DTOs.CollectionTags; using API.Entities.Metadata; using API.Extensions; using API.Services; -using API.Services.Tasks.Metadata; -using API.SignalR; using Kavita.Common; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -35,16 +33,17 @@ public class CollectionController : BaseApiController /// /// [HttpGet] - public async Task> GetAllTags() + public async Task>> GetAllTags() { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); + if (user == null) return Unauthorized(); var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user); if (isAdmin) { - return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync(); + return Ok(await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync()); } - return await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync(user.Id); + return Ok(await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync(user.Id)); } /// @@ -55,13 +54,13 @@ public class CollectionController : BaseApiController /// [Authorize(Policy = "RequireAdminRole")] [HttpGet("search")] - public async Task> SearchTags(string queryString) + public async Task>> SearchTags(string queryString) { queryString ??= string.Empty; queryString = queryString.Replace(@"%", string.Empty); if (queryString.Length == 0) return await GetAllTags(); - return await _unitOfWork.CollectionTagRepository.SearchTagDtosAsync(queryString, User.GetUserId()); + return Ok(await _unitOfWork.CollectionTagRepository.SearchTagDtosAsync(queryString, User.GetUserId())); } /// @@ -126,7 +125,7 @@ public class CollectionController : BaseApiController { try { - var tag = await _unitOfWork.CollectionTagRepository.GetFullTagAsync(updateSeriesForTagDto.Tag.Id); + var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(updateSeriesForTagDto.Tag.Id, CollectionTagIncludes.SeriesMetadata); if (tag == null) return BadRequest("Not a valid Tag"); tag.SeriesMetadatas ??= new List(); diff --git a/API/Controllers/DeviceController.cs b/API/Controllers/DeviceController.cs index 3d67d2d7f..d709020eb 100644 --- a/API/Controllers/DeviceController.cs +++ b/API/Controllers/DeviceController.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using System.Threading; using System.Threading.Tasks; using API.Data; using API.Data.Repositories; @@ -9,9 +7,7 @@ using API.DTOs.Device; using API.Extensions; using API.Services; using API.SignalR; -using ExCSS; using Kavita.Common; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace API.Controllers; @@ -39,6 +35,7 @@ public class DeviceController : BaseApiController public async Task CreateOrUpdateDevice(CreateDeviceDto dto) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Devices); + if (user == null) return Unauthorized(); var device = await _deviceService.Create(dto, user); if (device == null) return BadRequest("There was an error when creating the device"); @@ -50,6 +47,7 @@ public class DeviceController : BaseApiController public async Task UpdateDevice(UpdateDeviceDto dto) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Devices); + if (user == null) return Unauthorized(); var device = await _deviceService.Update(dto, user); if (device == null) return BadRequest("There was an error when updating the device"); @@ -67,6 +65,7 @@ public class DeviceController : BaseApiController { if (deviceId <= 0) return BadRequest("Not a valid deviceId"); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Devices); + if (user == null) return Unauthorized(); if (await _deviceService.Delete(user, deviceId)) return Ok(); return BadRequest("Could not delete device"); diff --git a/API/Controllers/DownloadController.cs b/API/Controllers/DownloadController.cs index 6c44c2659..be1db4969 100644 --- a/API/Controllers/DownloadController.cs +++ b/API/Controllers/DownloadController.cs @@ -93,13 +93,13 @@ public class DownloadController : BaseApiController public async Task DownloadVolume(int volumeId) { if (!await HasDownloadPermission()) return BadRequest("You do not have permission"); - - var files = await _unitOfWork.VolumeRepository.GetFilesForVolume(volumeId); var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(volumeId); + if (volume == null) return BadRequest("Volume doesn't exist"); + var files = await _unitOfWork.VolumeRepository.GetFilesForVolume(volumeId); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId); try { - return await DownloadFiles(files, $"download_{User.GetUsername()}_v{volumeId}", $"{series.Name} - Volume {volume.Number}.zip"); + return await DownloadFiles(files, $"download_{User.GetUsername()}_v{volumeId}", $"{series!.Name} - Volume {volume.Number}.zip"); } catch (KavitaException ex) { @@ -110,6 +110,7 @@ public class DownloadController : BaseApiController private async Task HasDownloadPermission() { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); + if (user == null) return false; return await _accountService.HasDownloadPermission(user); } @@ -130,11 +131,12 @@ public class DownloadController : BaseApiController if (!await HasDownloadPermission()) return BadRequest("You do not have permission"); var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId); var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId); + if (chapter == null) return BadRequest("Invalid chapter"); var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapter.VolumeId); - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume!.SeriesId); try { - return await DownloadFiles(files, $"download_{User.GetUsername()}_c{chapterId}", $"{series.Name} - Chapter {chapter.Number}.zip"); + return await DownloadFiles(files, $"download_{User.GetUsername()}_c{chapterId}", $"{series!.Name} - Chapter {chapter.Number}.zip"); } catch (KavitaException ex) { @@ -177,8 +179,9 @@ public class DownloadController : BaseApiController public async Task DownloadSeries(int seriesId) { if (!await HasDownloadPermission()) return BadRequest("You do not have permission"); - var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); + if (series == null) return BadRequest("Invalid Series"); + var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId); try { return await DownloadFiles(files, $"download_{User.GetUsername()}_s{seriesId}", $"{series.Name}.zip"); @@ -201,13 +204,13 @@ public class DownloadController : BaseApiController if (!downloadBookmarkDto.Bookmarks.Any()) return BadRequest("Bookmarks cannot be empty"); // We know that all bookmarks will be for one single seriesId - var userId = User.GetUserId(); - var username = User.GetUsername(); + var userId = User.GetUserId()!; + var username = User.GetUsername()!; var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(downloadBookmarkDto.Bookmarks.First().SeriesId); var files = await _bookmarkService.GetBookmarkFilesById(downloadBookmarkDto.Bookmarks.Select(b => b.Id)); - var filename = $"{series.Name} - Bookmarks.zip"; + var filename = $"{series!.Name} - Bookmarks.zip"; await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.DownloadProgressEvent(username, Path.GetFileNameWithoutExtension(filename), 0F)); var seriesIds = string.Join("_", downloadBookmarkDto.Bookmarks.Select(b => b.SeriesId).Distinct()); diff --git a/API/Controllers/FallbackController.cs b/API/Controllers/FallbackController.cs index 2f5d7fceb..a765269b8 100644 --- a/API/Controllers/FallbackController.cs +++ b/API/Controllers/FallbackController.cs @@ -1,9 +1,7 @@ -using System; -using System.IO; +using System.IO; using API.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; namespace API.Controllers; diff --git a/API/Controllers/HealthController.cs b/API/Controllers/HealthController.cs index c0d44582f..27fe060ea 100644 --- a/API/Controllers/HealthController.cs +++ b/API/Controllers/HealthController.cs @@ -1,5 +1,3 @@ -using System; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index 1404d944c..9b9308b2c 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -7,7 +7,6 @@ using API.Data; using API.Data.Repositories; using API.DTOs; using API.DTOs.JumpBar; -using API.DTOs.Search; using API.DTOs.System; using API.Entities; using API.Entities.Enums; @@ -17,7 +16,6 @@ using API.Services; using API.Services.Tasks.Scanner; using API.SignalR; using AutoMapper; -using Kavita.Common; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; @@ -209,6 +207,7 @@ public class LibraryController : BaseApiController { var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(dto.ApiKey); var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + if (user == null) return Unauthorized(); // Validate user has Admin privileges var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user); @@ -244,7 +243,6 @@ public class LibraryController : BaseApiController try { - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.None); if (TaskScheduler.HasScanTaskRunningForLibrary(libraryId)) { _logger.LogInformation("User is attempting to delete a library while a scan is in progress"); @@ -252,6 +250,9 @@ public class LibraryController : BaseApiController "You cannot delete a library while a scan is in progress. Please wait for scan to complete or restart Kavita then try to delete"); } + var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); + if (library == null) return BadRequest("Library no longer exists"); + // 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, @@ -317,8 +318,10 @@ public class LibraryController : BaseApiController [HttpPost("update")] public async Task UpdateLibrary(UpdateLibraryDto dto) { - var newName = dto.Name.Trim(); var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(dto.Id, LibraryIncludes.Folders); + if (library == null) return BadRequest("Library doesn't exist"); + + var newName = dto.Name.Trim(); if (await _unitOfWork.LibraryRepository.LibraryExists(newName) && !library.Name.Equals(newName)) return BadRequest("Library name already exists"); diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index e39825893..57c88142f 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -194,6 +194,7 @@ public class OpdsController : BaseApiController return BadRequest("OPDS is not enabled on this server"); var userId = await GetUser(apiKey); var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + if (user == null) return Unauthorized(); var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user); IEnumerable tags = isAdmin ? (await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync()) @@ -230,6 +231,7 @@ public class OpdsController : BaseApiController return BadRequest("OPDS is not enabled on this server"); var userId = await GetUser(apiKey); var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); + if (user == null) return Unauthorized(); var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user); IEnumerable tags; @@ -310,7 +312,8 @@ public class OpdsController : BaseApiController var userId = await GetUser(apiKey); var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - var userWithLists = await _unitOfWork.UserRepository.GetUserByUsernameAsync(user.UserName, AppUserIncludes.ReadingListsWithItems); + var userWithLists = await _unitOfWork.UserRepository.GetUserByUsernameAsync(user!.UserName!, AppUserIncludes.ReadingListsWithItems); + if (userWithLists == null) return Unauthorized(); var readingList = userWithLists.ReadingLists.SingleOrDefault(t => t.Id == readingListId); if (readingList == null) { @@ -432,7 +435,6 @@ public class OpdsController : BaseApiController query = query.Replace(@"%", string.Empty); // Get libraries user has access to var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId)).ToList(); - if (!libraries.Any()) return BadRequest("User does not have access to any libraries"); var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user); @@ -569,7 +571,7 @@ public class OpdsController : BaseApiController (await _unitOfWork.ChapterRepository.GetChaptersAsync(volumeId)).OrderBy(x => double.Parse(x.Number), _chapterSortComparer); - var feed = CreateFeed(series.Name + " - Volume " + volume.Name + $" - {SeriesService.FormatChapterName(libraryType)}s ", $"{apiKey}/series/{seriesId}/volume/{volumeId}", apiKey); + var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {SeriesService.FormatChapterName(libraryType)}s ", $"{apiKey}/series/{seriesId}/volume/{volumeId}", apiKey); SetFeedId(feed, $"series-{series.Id}-volume-{volume.Id}-{SeriesService.FormatChapterName(libraryType)}s"); foreach (var chapter in chapters) { @@ -597,11 +599,12 @@ public class OpdsController : BaseApiController var userId = await GetUser(apiKey); var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId); - var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId); var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId); + if (chapter == null) return BadRequest("Chapter doesn't exist"); + var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId); var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId); - var feed = CreateFeed(series.Name + " - Volume " + volume.Name + $" - {SeriesService.FormatChapterName(libraryType)}s", $"{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}", apiKey); + var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {SeriesService.FormatChapterName(libraryType)}s", $"{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}", apiKey); SetFeedId(feed, $"series-{series.Id}-volume-{volumeId}-{SeriesService.FormatChapterName(libraryType)}-{chapterId}-files"); foreach (var mangaFile in files) { @@ -768,7 +771,7 @@ public class OpdsController : BaseApiController DirectoryService.GetHumanReadableBytes(_directoryService.GetTotalSize(new List() {mangaFile.FilePath})); var fileType = _downloadService.GetContentTypeFromFile(mangaFile.FilePath); - var filename = Uri.EscapeDataString(Path.GetFileName(mangaFile.FilePath) ?? string.Empty); + var filename = Uri.EscapeDataString(Path.GetFileName(mangaFile.FilePath)); var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId); var volume = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, await GetUser(apiKey)); @@ -890,7 +893,7 @@ public class OpdsController : BaseApiController return link; } - private static FeedLink CreateLink(string rel, string type, string href, string title = null) + private static FeedLink CreateLink(string rel, string type, string href, string? title = null) { return new FeedLink() { diff --git a/API/Controllers/PluginController.cs b/API/Controllers/PluginController.cs index 39f396985..e8659b6dc 100644 --- a/API/Controllers/PluginController.cs +++ b/API/Controllers/PluginController.cs @@ -38,10 +38,10 @@ public class PluginController : BaseApiController var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); if (userId <= 0) return Unauthorized(); var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - _logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({UserId})'s API Key", pluginName, user.UserName, userId); + _logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({UserId})'s API Key", pluginName, user!.UserName, userId); return new UserDto { - Username = user.UserName, + Username = user.UserName!, Token = await _tokenService.CreateToken(user), ApiKey = user.ApiKey, }; diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 79948ce60..842aa75db 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -13,7 +13,6 @@ using API.Entities; using API.Entities.Enums; using API.Extensions; using API.Services; -using API.Services.Tasks; using API.SignalR; using Hangfire; using Microsoft.AspNetCore.Authorization; @@ -164,7 +163,7 @@ public class ReaderController : BaseApiController [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"chapterId", "extractPdf"})] public async Task>> GetFileDimensions(int chapterId, bool extractPdf = false) { - if (chapterId <= 0) return null; + if (chapterId <= 0) return ArraySegment.Empty; var chapter = await _cacheService.Ensure(chapterId, extractPdf); if (chapter == null) return BadRequest("Could not find Chapter"); return Ok(_cacheService.GetCachedFileDimensions(chapterId)); @@ -179,9 +178,9 @@ public class ReaderController : BaseApiController /// [HttpGet("chapter-info")] [ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = new []{"chapterId", "extractPdf", "includeDimensions"})] - public async Task> GetChapterInfo(int chapterId, bool extractPdf = false, bool includeDimensions = false) + public async Task> GetChapterInfo(int chapterId, bool extractPdf = false, bool includeDimensions = false) { - if (chapterId <= 0) return null; // This can happen occasionally from UI, we should just ignore + if (chapterId <= 0) return Ok(null); // This can happen occasionally from UI, we should just ignore var chapter = await _cacheService.Ensure(chapterId, extractPdf); if (chapter == null) return BadRequest("Could not find Chapter"); @@ -249,7 +248,7 @@ public class ReaderController : BaseApiController return Ok(new BookmarkInfoDto() { - SeriesName = series.Name, + SeriesName = series!.Name, SeriesFormat = series.Format, SeriesId = series.Id, LibraryId = series.LibraryId, @@ -267,6 +266,7 @@ public class ReaderController : BaseApiController public async Task MarkRead(MarkReadDto markReadDto) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress); + if (user == null) return Unauthorized(); await _readerService.MarkSeriesAsRead(user, markReadDto.SeriesId); if (!await _unitOfWork.CommitAsync()) return BadRequest("There was an issue saving progress"); @@ -284,6 +284,7 @@ public class ReaderController : BaseApiController public async Task MarkUnread(MarkReadDto markReadDto) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress); + if (user == null) return Unauthorized(); await _readerService.MarkSeriesAsUnread(user, markReadDto.SeriesId); if (!await _unitOfWork.CommitAsync()) return BadRequest("There was an issue saving progress"); @@ -300,6 +301,7 @@ public class ReaderController : BaseApiController public async Task MarkVolumeAsUnread(MarkVolumeReadDto markVolumeReadDto) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress); + if (user == null) return Unauthorized(); var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId); await _readerService.MarkChaptersAsUnread(user, markVolumeReadDto.SeriesId, chapters); @@ -323,9 +325,10 @@ public class ReaderController : BaseApiController var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress); var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId); + if (user == null) return Unauthorized(); await _readerService.MarkChaptersAsRead(user, markVolumeReadDto.SeriesId, chapters); await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate, - MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, markVolumeReadDto.SeriesId, + MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName!, markVolumeReadDto.SeriesId, markVolumeReadDto.VolumeId, 0, chapters.Sum(c => c.Pages))); if (await _unitOfWork.CommitAsync()) @@ -346,6 +349,7 @@ public class ReaderController : BaseApiController public async Task MarkMultipleAsRead(MarkVolumesReadDto dto) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress); + if (user == null) return Unauthorized(); user.Progresses ??= new List(); var chapterIds = await _unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds); @@ -374,6 +378,7 @@ public class ReaderController : BaseApiController public async Task MarkMultipleAsUnread(MarkVolumesReadDto dto) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress); + if (user == null) return Unauthorized(); user.Progresses ??= new List(); var chapterIds = await _unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds); @@ -401,6 +406,7 @@ public class ReaderController : BaseApiController public async Task MarkMultipleSeriesAsRead(MarkMultipleSeriesAsReadDto dto) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress); + if (user == null) return Unauthorized(); user.Progresses ??= new List(); var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(dto.SeriesIds.ToArray(), true); @@ -426,6 +432,7 @@ public class ReaderController : BaseApiController public async Task MarkMultipleSeriesAsUnread(MarkMultipleSeriesAsReadDto dto) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress); + if (user == null) return Unauthorized(); user.Progresses ??= new List(); var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(dto.SeriesIds.ToArray(), true); @@ -509,6 +516,7 @@ public class ReaderController : BaseApiController public async Task> MarkChaptersUntilAsRead(int seriesId, float chapterNumber) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress); + if (user == null) return Unauthorized(); user.Progresses ??= new List(); // Tachiyomi sends chapter 0.0f when there's no chapters read. @@ -546,6 +554,7 @@ public class ReaderController : BaseApiController public async Task>> GetBookmarks(int chapterId) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); + if (user == null) return Unauthorized(); if (user.Bookmarks == null) return Ok(Array.Empty()); return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForChapter(user.Id, chapterId)); } @@ -559,6 +568,7 @@ public class ReaderController : BaseApiController public async Task>> GetAllBookmarks(FilterDto filterDto) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); + if (user == null) return Unauthorized(); if (user.Bookmarks == null) return Ok(Array.Empty()); return Ok(await _unitOfWork.UserRepository.GetAllBookmarkDtos(user.Id, filterDto)); @@ -573,6 +583,7 @@ public class ReaderController : BaseApiController public async Task RemoveBookmarks(RemoveBookmarkForSeriesDto dto) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); + if (user == null) return Unauthorized(); if (user.Bookmarks == null) return Ok("Nothing to remove"); try @@ -612,7 +623,8 @@ public class ReaderController : BaseApiController public async Task BulkRemoveBookmarks(BulkRemoveBookmarkForSeriesDto dto) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); - if (user.Bookmarks == null) return Ok("Nothing to remove"); + if (user == null) return Unauthorized(); + if (user?.Bookmarks == null) return Ok("Nothing to remove"); try { @@ -648,7 +660,8 @@ public class ReaderController : BaseApiController public async Task>> GetBookmarksForVolume(int volumeId) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); - if (user.Bookmarks == null) return Ok(Array.Empty()); + if (user == null) return Unauthorized(); + if (user?.Bookmarks == null) return Ok(Array.Empty()); return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForVolume(user.Id, volumeId)); } @@ -661,7 +674,8 @@ public class ReaderController : BaseApiController public async Task>> GetBookmarksForSeries(int seriesId) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); - if (user.Bookmarks == null) return Ok(Array.Empty()); + if (user == null) return Unauthorized(); + if (user?.Bookmarks == null) return Ok(Array.Empty()); return Ok(await _unitOfWork.UserRepository.GetBookmarkDtosForSeries(user.Id, seriesId)); } diff --git a/API/Controllers/ReadingListController.cs b/API/Controllers/ReadingListController.cs index 34e9b7b1f..e94223f43 100644 --- a/API/Controllers/ReadingListController.cs +++ b/API/Controllers/ReadingListController.cs @@ -1,22 +1,15 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Abstractions; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Comparators; using API.Data; using API.Data.Repositories; using API.DTOs.ReadingLists; -using API.DTOs.ReadingLists.CBL; -using API.Entities; using API.Extensions; using API.Helpers; using API.Services; using API.SignalR; using Kavita.Common; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace API.Controllers; @@ -186,8 +179,8 @@ public class ReadingListController : BaseApiController [HttpPost("create")] public async Task> CreateList(CreateReadingListDto dto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.ReadingLists); + if (user == null) return Unauthorized(); try { diff --git a/API/Controllers/RecommendedController.cs b/API/Controllers/RecommendedController.cs index f945c735d..a8726e9c1 100644 --- a/API/Controllers/RecommendedController.cs +++ b/API/Controllers/RecommendedController.cs @@ -58,7 +58,7 @@ public class RecommendedController : BaseApiController [HttpGet("highly-rated")] public async Task>> GetHighlyRated(int libraryId, [FromQuery] UserParams userParams) { - var userId = User.GetUserId(); + var userId = User.GetUserId()!; userParams ??= new UserParams(); var series = await _unitOfWork.SeriesRepository.GetHighlyRated(userId, libraryId, userParams); await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series); diff --git a/API/Controllers/SearchController.cs b/API/Controllers/SearchController.cs index 62ad278c5..84d2bbf3b 100644 --- a/API/Controllers/SearchController.cs +++ b/API/Controllers/SearchController.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Linq; using System.Threading.Tasks; using API.Data; using API.Data.Repositories; @@ -54,6 +53,7 @@ public class SearchController : BaseApiController queryString = Services.Tasks.Scanner.Parser.Parser.CleanQuery(queryString); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); + if (user == null) return Unauthorized(); var libraries = _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(user.Id, QueryContext.Search).ToList(); if (!libraries.Any()) return BadRequest("User does not have access to any libraries"); diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index 6a04a129c..ee5208ea8 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using API.Constants; using API.Data; @@ -11,7 +10,6 @@ using API.DTOs.Metadata; using API.DTOs.SeriesDetail; using API.Entities; using API.Entities.Enums; -using API.Entities.Metadata; using API.Extensions; using API.Helpers; using API.Services; @@ -137,7 +135,7 @@ public class SeriesController : BaseApiController public async Task UpdateSeriesRating(UpdateSeriesRatingDto updateSeriesRatingDto) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Ratings); - if (!await _seriesService.UpdateRating(user, updateSeriesRatingDto)) return BadRequest("There was a critical error."); + if (!await _seriesService.UpdateRating(user!, updateSeriesRatingDto)) return BadRequest("There was a critical error."); return Ok(); } @@ -159,14 +157,14 @@ public class SeriesController : BaseApiController } series.Name = updateSeries.Name.Trim(); - series.NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(series.Name); - if (!string.IsNullOrEmpty(updateSeries.SortName.Trim())) + series.NormalizedName = series.Name.ToNormalized(); + if (!string.IsNullOrEmpty(updateSeries.SortName?.Trim())) { series.SortName = updateSeries.SortName.Trim(); } - series.LocalizedName = updateSeries.LocalizedName.Trim(); - series.NormalizedLocalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(series.LocalizedName); + series.LocalizedName = updateSeries.LocalizedName?.Trim(); + series.NormalizedLocalizedName = series.LocalizedName?.ToNormalized(); series.NameLocked = updateSeries.NameLocked; series.SortNameLocked = updateSeries.SortNameLocked; diff --git a/API/Controllers/ServerController.cs b/API/Controllers/ServerController.cs index 0cae64323..8a3b1f08b 100644 --- a/API/Controllers/ServerController.cs +++ b/API/Controllers/ServerController.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; @@ -8,7 +7,6 @@ using API.DTOs.Jobs; using API.DTOs.Stats; using API.DTOs.Update; using API.Extensions; -using API.Logging; using API.Services; using API.Services.Tasks; using Hangfire; @@ -16,7 +14,6 @@ using Hangfire.Storage; using Kavita.Common; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using TaskScheduler = API.Services.TaskScheduler; diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs index 374a1a4a5..9d80248dc 100644 --- a/API/Controllers/SettingsController.cs +++ b/API/Controllers/SettingsController.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Net; using System.Threading.Tasks; diff --git a/API/Controllers/StatsController.cs b/API/Controllers/StatsController.cs index f315b6f3f..ec8588d56 100644 --- a/API/Controllers/StatsController.cs +++ b/API/Controllers/StatsController.cs @@ -32,7 +32,7 @@ public class StatsController : BaseApiController public async Task> GetUserReadStatistics(int userId) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); - if (user.Id != userId && !await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole)) + if (user!.Id != userId && !await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole)) return Unauthorized("You are not authorized to view another user's statistics"); return Ok(await _statService.GetUserReadStatistics(userId, new List())); @@ -116,7 +116,7 @@ public class StatsController : BaseApiController { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); var isAdmin = User.IsInRole(PolicyConstants.AdminRole); - if (!isAdmin && userId != user.Id) return BadRequest(); + if (!isAdmin && userId != user!.Id) return BadRequest(); return Ok(await _statService.ReadCountByDay(userId, days)); } @@ -136,7 +136,7 @@ public class StatsController : BaseApiController { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); var isAdmin = User.IsInRole(PolicyConstants.AdminRole); - if (!isAdmin && userId != user.Id) return BadRequest(); + if (!isAdmin && userId != user!.Id) return BadRequest(); return Ok(await _statService.GetReadingHistory(userId)); } diff --git a/API/Controllers/TachiyomiController.cs b/API/Controllers/TachiyomiController.cs index 84bde35d1..ef24d05ff 100644 --- a/API/Controllers/TachiyomiController.cs +++ b/API/Controllers/TachiyomiController.cs @@ -43,7 +43,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(User.GetUsername(), AppUserIncludes.Progress); + var user = (await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), + AppUserIncludes.Progress))!; return Ok(await _tachiyomiService.MarkChaptersUntilAsRead(user, seriesId, chapterNumber)); } } diff --git a/API/Controllers/ThemeController.cs b/API/Controllers/ThemeController.cs index d6a9b526e..dc697a89d 100644 --- a/API/Controllers/ThemeController.cs +++ b/API/Controllers/ThemeController.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using API.Data; using API.DTOs.Theme; -using API.Extensions; using API.Services; using API.Services.Tasks; using Kavita.Common; diff --git a/API/Controllers/UploadController.cs b/API/Controllers/UploadController.cs index 5a7ed5403..465a143cd 100644 --- a/API/Controllers/UploadController.cs +++ b/API/Controllers/UploadController.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Threading.Tasks; using API.Data; using API.DTOs.Uploads; @@ -10,7 +9,6 @@ using Flurl.Http; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using NetVips; namespace API.Controllers; @@ -93,8 +91,10 @@ public class UploadController : BaseApiController try { - var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, ImageService.GetSeriesFormat(uploadFileDto.Id)); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(uploadFileDto.Id); + if (series == null) return BadRequest("Invalid Series"); + var convertToWebP = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertCoverToWebP; + var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, ImageService.GetSeriesFormat(uploadFileDto.Id), convertToWebP); if (!string.IsNullOrEmpty(filePath)) { @@ -140,8 +140,10 @@ public class UploadController : BaseApiController try { - var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetCollectionTagFormat(uploadFileDto.Id)}"); var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(uploadFileDto.Id); + if (tag == null) return BadRequest("Invalid Tag id"); + var convertToWebP = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertCoverToWebP; + var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetCollectionTagFormat(uploadFileDto.Id)}", convertToWebP); if (!string.IsNullOrEmpty(filePath)) { @@ -190,8 +192,10 @@ public class UploadController : BaseApiController try { - var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetReadingListFormat(uploadFileDto.Id)}"); var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(uploadFileDto.Id); + if (readingList == null) return BadRequest("Reading list is not valid"); + var convertToWebP = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertCoverToWebP; + var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetReadingListFormat(uploadFileDto.Id)}", convertToWebP); if (!string.IsNullOrEmpty(filePath)) { @@ -238,7 +242,9 @@ public class UploadController : BaseApiController try { var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id); - var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetChapterFormat(uploadFileDto.Id, chapter.VolumeId)}"); + if (chapter == null) return BadRequest("Invalid Chapter"); + var convertToWebP = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertCoverToWebP; + var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetChapterFormat(uploadFileDto.Id, chapter.VolumeId)}", convertToWebP); if (!string.IsNullOrEmpty(filePath)) { @@ -246,8 +252,11 @@ public class UploadController : BaseApiController chapter.CoverImageLocked = true; _unitOfWork.ChapterRepository.Update(chapter); var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(chapter.VolumeId); - volume.CoverImage = chapter.CoverImage; - _unitOfWork.VolumeRepository.Update(volume); + if (volume != null) + { + volume.CoverImage = chapter.CoverImage; + _unitOfWork.VolumeRepository.Update(volume); + } } if (_unitOfWork.HasChanges()) @@ -301,8 +310,9 @@ public class UploadController : BaseApiController try { + var convertToWebP = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertCoverToWebP; var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, - $"{ImageService.GetLibraryFormat(uploadFileDto.Id)}", ImageService.LibraryThumbnailWidth); + $"{ImageService.GetLibraryFormat(uploadFileDto.Id)}", convertToWebP, ImageService.LibraryThumbnailWidth); if (!string.IsNullOrEmpty(filePath)) { @@ -340,19 +350,20 @@ public class UploadController : BaseApiController try { var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id); + if (chapter == null) return BadRequest("Chapter no longer exists"); var originalFile = chapter.CoverImage; chapter.CoverImage = string.Empty; chapter.CoverImageLocked = false; _unitOfWork.ChapterRepository.Update(chapter); - var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(chapter.VolumeId); + var volume = (await _unitOfWork.VolumeRepository.GetVolumeAsync(chapter.VolumeId))!; volume.CoverImage = chapter.CoverImage; _unitOfWork.VolumeRepository.Update(volume); - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId); + var series = (await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId))!; if (_unitOfWork.HasChanges()) { await _unitOfWork.CommitAsync(); - System.IO.File.Delete(originalFile); + if (originalFile != null) System.IO.File.Delete(originalFile); _taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id, true); return Ok(); } diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index 6fd8249e4..413a47cb0 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -4,10 +4,7 @@ using System.Threading.Tasks; using API.Data; using API.Data.Repositories; using API.DTOs; -using API.DTOs.Filtering; -using API.Entities.Enums; using API.Extensions; -using API.Helpers; using API.SignalR; using AutoMapper; using Microsoft.AspNetCore.Authorization; @@ -68,6 +65,7 @@ public class UsersController : BaseApiController { var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); + if (library == null) return BadRequest("Library does not exist"); return Ok(await _unitOfWork.AppUserProgressRepository.UserHasProgress(library.Type, userId)); } @@ -83,9 +81,8 @@ public class UsersController : BaseApiController { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.UserPreferences); - var existingPreferences = user.UserPreferences; - - preferencesDto.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme(); + if (user == null) return Unauthorized(); + var existingPreferences = user!.UserPreferences; existingPreferences.ReadingDirection = preferencesDto.ReadingDirection; existingPreferences.ScalingOption = preferencesDto.ScalingOption; @@ -107,8 +104,8 @@ public class UsersController : BaseApiController existingPreferences.BookReaderImmersiveMode = preferencesDto.BookReaderImmersiveMode; existingPreferences.GlobalPageLayoutMode = preferencesDto.GlobalPageLayoutMode; existingPreferences.BlurUnreadSummaries = preferencesDto.BlurUnreadSummaries; - existingPreferences.Theme = await _unitOfWork.SiteThemeRepository.GetThemeById(preferencesDto.Theme.Id); existingPreferences.LayoutMode = preferencesDto.LayoutMode; + existingPreferences.Theme = preferencesDto.Theme ?? await _unitOfWork.SiteThemeRepository.GetDefaultTheme(); existingPreferences.PromptForDownloadSize = preferencesDto.PromptForDownloadSize; existingPreferences.NoTransitions = preferencesDto.NoTransitions; existingPreferences.SwipeToPaginate = preferencesDto.SwipeToPaginate; @@ -117,7 +114,7 @@ public class UsersController : BaseApiController if (await _unitOfWork.CommitAsync()) { - 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); } diff --git a/API/Controllers/WantToReadController.cs b/API/Controllers/WantToReadController.cs index 424681cf4..c330abb70 100644 --- a/API/Controllers/WantToReadController.cs +++ b/API/Controllers/WantToReadController.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Linq; +using System.Linq; using System.Threading.Tasks; using API.Data; using API.Data.Repositories; @@ -56,6 +55,7 @@ public class WantToReadController : BaseApiController { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.WantToRead); + if (user == null) return Unauthorized(); var existingIds = user.WantToRead.Select(s => s.Id).ToList(); existingIds.AddRange(dto.SeriesIds); @@ -84,6 +84,7 @@ public class WantToReadController : BaseApiController { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.WantToRead); + if (user == null) return Unauthorized(); user.WantToRead = user.WantToRead.Where(s => !dto.SeriesIds.Contains(s.Id)).ToList(); diff --git a/API/DTOs/Account/ConfirmEmailDto.cs b/API/DTOs/Account/ConfirmEmailDto.cs index 225835796..fb9a7c470 100644 --- a/API/DTOs/Account/ConfirmEmailDto.cs +++ b/API/DTOs/Account/ConfirmEmailDto.cs @@ -5,12 +5,12 @@ namespace API.DTOs.Account; public class ConfirmEmailDto { [Required] - public string Email { get; set; } + public string Email { get; set; } = default!; [Required] - public string Token { get; set; } + public string Token { get; set; } = default!; [Required] [StringLength(32, MinimumLength = 6)] - public string Password { get; set; } + public string Password { get; set; } = default!; [Required] - public string Username { get; set; } + public string Username { get; set; } = default!; } diff --git a/API/DTOs/Account/ConfirmEmailUpdateDto.cs b/API/DTOs/Account/ConfirmEmailUpdateDto.cs index 63d31340a..42abb1295 100644 --- a/API/DTOs/Account/ConfirmEmailUpdateDto.cs +++ b/API/DTOs/Account/ConfirmEmailUpdateDto.cs @@ -5,7 +5,7 @@ namespace API.DTOs.Account; public class ConfirmEmailUpdateDto { [Required] - public string Email { get; set; } + public string Email { get; set; } = default!; [Required] - public string Token { get; set; } + public string Token { get; set; } = default!; } diff --git a/API/DTOs/Account/ConfirmMigrationEmailDto.cs b/API/DTOs/Account/ConfirmMigrationEmailDto.cs index 07e0aa1ca..efb42b8fd 100644 --- a/API/DTOs/Account/ConfirmMigrationEmailDto.cs +++ b/API/DTOs/Account/ConfirmMigrationEmailDto.cs @@ -2,6 +2,6 @@ public class ConfirmMigrationEmailDto { - public string Email { get; set; } - public string Token { get; set; } + public string Email { get; set; } = default!; + public string Token { get; set; } = default!; } diff --git a/API/DTOs/Account/ConfirmPasswordResetDto.cs b/API/DTOs/Account/ConfirmPasswordResetDto.cs index 603508ac4..862a18986 100644 --- a/API/DTOs/Account/ConfirmPasswordResetDto.cs +++ b/API/DTOs/Account/ConfirmPasswordResetDto.cs @@ -5,10 +5,10 @@ namespace API.DTOs.Account; public class ConfirmPasswordResetDto { [Required] - public string Email { get; set; } + public string Email { get; set; } = default!; [Required] - public string Token { get; set; } + public string Token { get; set; } = default!; [Required] [StringLength(32, MinimumLength = 6)] - public string Password { get; set; } + public string Password { get; set; } = default!; } diff --git a/API/DTOs/Account/InviteUserDto.cs b/API/DTOs/Account/InviteUserDto.cs index 9532b86dd..112013053 100644 --- a/API/DTOs/Account/InviteUserDto.cs +++ b/API/DTOs/Account/InviteUserDto.cs @@ -1,24 +1,23 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using API.Entities.Enums; namespace API.DTOs.Account; public class InviteUserDto { [Required] - public string Email { get; set; } + public string Email { get; set; } = default!; /// /// List of Roles to assign to user. If admin not present, Pleb will be applied. /// If admin present, all libraries will be granted access and will ignore those from DTO. /// - public ICollection Roles { get; init; } + public ICollection Roles { get; init; } = default!; /// /// A list of libraries to grant access to /// - public IList Libraries { get; init; } + public IList Libraries { get; init; } = default!; /// /// An Age Rating which will limit the account to seeing everything equal to or below said rating. /// - public AgeRestrictionDto AgeRestriction { get; set; } + public AgeRestrictionDto AgeRestriction { get; set; } = default!; } diff --git a/API/DTOs/Account/InviteUserResponse.cs b/API/DTOs/Account/InviteUserResponse.cs index 9387b5492..97d7f408c 100644 --- a/API/DTOs/Account/InviteUserResponse.cs +++ b/API/DTOs/Account/InviteUserResponse.cs @@ -5,9 +5,9 @@ public class InviteUserResponse /// /// Email link used to setup the user account /// - public string EmailLink { get; set; } + public string EmailLink { get; set; } = default!; /// /// Was an email sent (ie is this server accessible) /// - public bool EmailSent { get; set; } + public bool EmailSent { get; set; } = default!; } diff --git a/API/DTOs/Account/LoginDto.cs b/API/DTOs/Account/LoginDto.cs index 44ccc5fc5..111db06d3 100644 --- a/API/DTOs/Account/LoginDto.cs +++ b/API/DTOs/Account/LoginDto.cs @@ -2,6 +2,6 @@ public class LoginDto { - public string Username { get; init; } - public string Password { get; set; } + public string Username { get; init; } = default!; + public string Password { get; set; } = default!; } diff --git a/API/DTOs/Account/MigrateUserEmailDto.cs b/API/DTOs/Account/MigrateUserEmailDto.cs index aa947d5d1..60d042165 100644 --- a/API/DTOs/Account/MigrateUserEmailDto.cs +++ b/API/DTOs/Account/MigrateUserEmailDto.cs @@ -2,8 +2,7 @@ public class MigrateUserEmailDto { - public string Email { get; set; } - public string Username { get; set; } - public string Password { get; set; } - public bool SendEmail { get; set; } + public string Email { get; set; } = default!; + public string Username { get; set; } = default!; + public string Password { get; set; } = default!; } diff --git a/API/DTOs/Account/ResetPasswordDto.cs b/API/DTOs/Account/ResetPasswordDto.cs index 9fa42d8ac..fc7147f62 100644 --- a/API/DTOs/Account/ResetPasswordDto.cs +++ b/API/DTOs/Account/ResetPasswordDto.cs @@ -8,15 +8,15 @@ public class ResetPasswordDto /// The Username of the User /// [Required] - public string UserName { get; init; } + public string UserName { get; init; } = default!; /// /// The new password /// [Required] [StringLength(32, MinimumLength = 6)] - public string Password { get; init; } + public string Password { get; init; } = default!; /// /// The old, existing password. If an admin is performing the change, this is not required. Otherwise, it is. /// - public string OldPassword { get; init; } + public string OldPassword { get; init; } = default!; } diff --git a/API/DTOs/Account/TokenRequestDto.cs b/API/DTOs/Account/TokenRequestDto.cs index 508e0c75c..85ab9f87a 100644 --- a/API/DTOs/Account/TokenRequestDto.cs +++ b/API/DTOs/Account/TokenRequestDto.cs @@ -2,6 +2,6 @@ public class TokenRequestDto { - public string Token { get; init; } - public string RefreshToken { get; init; } + public string Token { get; init; } = default!; + public string RefreshToken { get; init; } = default!; } diff --git a/API/DTOs/Account/UpdateEmailDto.cs b/API/DTOs/Account/UpdateEmailDto.cs index 2363790f6..eac06be53 100644 --- a/API/DTOs/Account/UpdateEmailDto.cs +++ b/API/DTOs/Account/UpdateEmailDto.cs @@ -2,6 +2,6 @@ public class UpdateEmailDto { - public string Email { get; set; } - public string Password { get; set; } + public string Email { get; set; } = default!; + public string Password { get; set; } = default!; } diff --git a/API/DTOs/Account/UpdateUserDto.cs b/API/DTOs/Account/UpdateUserDto.cs index 7a928690c..4941e8d8a 100644 --- a/API/DTOs/Account/UpdateUserDto.cs +++ b/API/DTOs/Account/UpdateUserDto.cs @@ -1,24 +1,21 @@ using System.Collections.Generic; -using System.Text.Json.Serialization; -using API.Entities.Enums; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal; namespace API.DTOs.Account; public record UpdateUserDto { public int UserId { get; set; } - public string Username { get; set; } + public string Username { get; set; } = default!; /// List of Roles to assign to user. If admin not present, Pleb will be applied. /// If admin present, all libraries will be granted access and will ignore those from DTO. - public IList Roles { get; init; } + public IList Roles { get; init; } = default!; /// /// A list of libraries to grant access to /// - public IList Libraries { get; init; } + public IList Libraries { get; init; } = default!; /// /// An Age Rating which will limit the account to seeing everything equal to or below said rating. /// - public AgeRestrictionDto AgeRestriction { get; init; } + public AgeRestrictionDto AgeRestriction { get; init; } = default!; } diff --git a/API/DTOs/ChapterDto.cs b/API/DTOs/ChapterDto.cs index 48d05ad2e..4804554e8 100644 --- a/API/DTOs/ChapterDto.cs +++ b/API/DTOs/ChapterDto.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using API.Entities.Enums; using API.Entities.Interfaces; -using Microsoft.AspNetCore.Mvc.RazorPages; namespace API.DTOs; @@ -16,11 +15,11 @@ public class ChapterDto : IHasReadTimeEstimate, IEntityDate /// /// Range of chapters. Chapter 2-4 -> "2-4". Chapter 2 -> "2". /// - public string Range { get; init; } + public string Range { get; init; } = default!; /// /// Smallest number of the Range. /// - public string Number { get; init; } + public string Number { get; init; } = default!; /// /// Total number of pages in all MangaFiles /// @@ -32,11 +31,11 @@ public class ChapterDto : IHasReadTimeEstimate, IEntityDate /// /// Used for books/specials to display custom title. For non-specials/books, will be set to /// - public string Title { get; set; } + public string Title { get; set; } = default!; /// /// The files that represent this Chapter /// - public ICollection Files { get; init; } + public ICollection Files { get; init; } = default!; /// /// Calculated at API time. Number of pages read for this Chapter for logged in user. /// @@ -69,12 +68,12 @@ public class ChapterDto : IHasReadTimeEstimate, IEntityDate /// Title of the Chapter/Issue /// /// Metadata field - public string TitleName { get; set; } + public string TitleName { get; set; } = default!; /// /// Summary of the Chapter /// /// This is not set normally, only for Series Detail - public string Summary { get; init; } + public string Summary { get; init; } = default!; /// /// Age Rating for the issue/chapter /// diff --git a/API/DTOs/CollectionTags/CollectionTagBulkAddDto.cs b/API/DTOs/CollectionTags/CollectionTagBulkAddDto.cs index 7b9ebc94d..1d078959d 100644 --- a/API/DTOs/CollectionTags/CollectionTagBulkAddDto.cs +++ b/API/DTOs/CollectionTags/CollectionTagBulkAddDto.cs @@ -9,9 +9,9 @@ public class CollectionTagBulkAddDto /// /// Can be 0 which then will use Title to create a tag public int CollectionTagId { get; init; } - public string CollectionTagTitle { get; init; } + public string CollectionTagTitle { get; init; } = default!; /// /// Series Ids to add onto Collection Tag /// - public IEnumerable SeriesIds { get; init; } + public IEnumerable SeriesIds { get; init; } = default!; } diff --git a/API/DTOs/CollectionTags/CollectionTagDto.cs b/API/DTOs/CollectionTags/CollectionTagDto.cs index 8cb68cc06..2a1279a35 100644 --- a/API/DTOs/CollectionTags/CollectionTagDto.cs +++ b/API/DTOs/CollectionTags/CollectionTagDto.cs @@ -3,12 +3,12 @@ public class CollectionTagDto { public int Id { get; set; } - public string Title { get; set; } - public string Summary { get; set; } + public string Title { get; set; } = default!; + public string Summary { get; set; } = default!; public bool Promoted { get; set; } /// /// The cover image string. This is used on Frontend to show or hide the Cover Image /// - public string CoverImage { get; set; } + public string CoverImage { get; set; } = default!; public bool CoverImageLocked { get; set; } } diff --git a/API/DTOs/CollectionTags/UpdateSeriesForTagDto.cs b/API/DTOs/CollectionTags/UpdateSeriesForTagDto.cs index 2381df285..9d6f2a035 100644 --- a/API/DTOs/CollectionTags/UpdateSeriesForTagDto.cs +++ b/API/DTOs/CollectionTags/UpdateSeriesForTagDto.cs @@ -4,6 +4,6 @@ namespace API.DTOs.CollectionTags; public class UpdateSeriesForTagDto { - public CollectionTagDto Tag { get; init; } - public IEnumerable SeriesIdsToRemove { get; init; } + public CollectionTagDto Tag { get; init; } = default!; + public IEnumerable SeriesIdsToRemove { get; init; } = default!; } diff --git a/API/DTOs/CreateLibraryDto.cs b/API/DTOs/CreateLibraryDto.cs index 151bcfeba..76584eaff 100644 --- a/API/DTOs/CreateLibraryDto.cs +++ b/API/DTOs/CreateLibraryDto.cs @@ -7,10 +7,10 @@ namespace API.DTOs; public class CreateLibraryDto { [Required] - public string Name { get; init; } + public string Name { get; init; } = default!; [Required] public LibraryType Type { get; init; } [Required] [MinLength(1)] - public IEnumerable Folders { get; init; } + public IEnumerable Folders { get; init; } = default!; } diff --git a/API/DTOs/DeleteSeriesDto.cs b/API/DTOs/DeleteSeriesDto.cs index a363d0568..12687fc25 100644 --- a/API/DTOs/DeleteSeriesDto.cs +++ b/API/DTOs/DeleteSeriesDto.cs @@ -4,5 +4,5 @@ namespace API.DTOs; public class DeleteSeriesDto { - public IList SeriesIds { get; set; } + public IList SeriesIds { get; set; } = default!; } diff --git a/API/DTOs/Device/CreateDeviceDto.cs b/API/DTOs/Device/CreateDeviceDto.cs index bdcdde194..7e59483fa 100644 --- a/API/DTOs/Device/CreateDeviceDto.cs +++ b/API/DTOs/Device/CreateDeviceDto.cs @@ -1,5 +1,4 @@ using System.ComponentModel.DataAnnotations; -using System.Runtime.InteropServices; using API.Entities.Enums.Device; namespace API.DTOs.Device; @@ -7,14 +6,14 @@ namespace API.DTOs.Device; public class CreateDeviceDto { [Required] - public string Name { get; set; } + public string Name { get; set; } = default!; /// /// Platform of the device. If not know, defaults to "Custom" /// [Required] public DevicePlatform Platform { get; set; } [Required] - public string EmailAddress { get; set; } + public string EmailAddress { get; set; } = default!; } diff --git a/API/DTOs/Device/DeviceDto.cs b/API/DTOs/Device/DeviceDto.cs index c05671113..069a7a4d2 100644 --- a/API/DTOs/Device/DeviceDto.cs +++ b/API/DTOs/Device/DeviceDto.cs @@ -17,11 +17,11 @@ public class DeviceDto /// /// If this device is web, this will be the browser name /// Pixel 3a, John's Kindle - public string Name { get; set; } + public string Name { get; set; } = default!; /// /// An email address associated with the device (ie Kindle). Will be used with Send to functionality /// - public string EmailAddress { get; set; } + public string EmailAddress { get; set; } = default!; /// /// Platform (ie) Windows 10 /// diff --git a/API/DTOs/Device/SendToDeviceDto.cs b/API/DTOs/Device/SendToDeviceDto.cs index 411f20ea0..fd88eaf59 100644 --- a/API/DTOs/Device/SendToDeviceDto.cs +++ b/API/DTOs/Device/SendToDeviceDto.cs @@ -5,5 +5,5 @@ namespace API.DTOs.Device; public class SendToDeviceDto { public int DeviceId { get; set; } - public IReadOnlyList ChapterIds { get; set; } + public IReadOnlyList ChapterIds { get; set; } = default!; } diff --git a/API/DTOs/Device/UpdateDeviceDto.cs b/API/DTOs/Device/UpdateDeviceDto.cs index 201adcb5d..d28d372c3 100644 --- a/API/DTOs/Device/UpdateDeviceDto.cs +++ b/API/DTOs/Device/UpdateDeviceDto.cs @@ -8,12 +8,12 @@ public class UpdateDeviceDto [Required] public int Id { get; set; } [Required] - public string Name { get; set; } + public string Name { get; set; } = default!; /// /// Platform of the device. If not know, defaults to "Custom" /// [Required] public DevicePlatform Platform { get; set; } [Required] - public string EmailAddress { get; set; } + public string EmailAddress { get; set; } = default!; } diff --git a/API/DTOs/Downloads/DownloadBookmarkDto.cs b/API/DTOs/Downloads/DownloadBookmarkDto.cs index d70cd25ac..5b7240b68 100644 --- a/API/DTOs/Downloads/DownloadBookmarkDto.cs +++ b/API/DTOs/Downloads/DownloadBookmarkDto.cs @@ -7,5 +7,5 @@ namespace API.DTOs.Downloads; public class DownloadBookmarkDto { [Required] - public IEnumerable Bookmarks { get; set; } + public IEnumerable Bookmarks { get; set; } = default!; } diff --git a/API/DTOs/Email/ConfirmationEmailDto.cs b/API/DTOs/Email/ConfirmationEmailDto.cs index a64d92f91..1a48c9974 100644 --- a/API/DTOs/Email/ConfirmationEmailDto.cs +++ b/API/DTOs/Email/ConfirmationEmailDto.cs @@ -2,11 +2,11 @@ public class ConfirmationEmailDto { - public string InvitingUser { get; init; } - public string EmailAddress { get; init; } - public string ServerConfirmationLink { get; init; } + public string InvitingUser { get; init; } = default!; + public string EmailAddress { get; init; } = default!; + public string ServerConfirmationLink { get; init; } = default!; /// /// InstallId of this Kavita Instance /// - public string InstallId { get; init; } + public string InstallId { get; init; } = default!; } diff --git a/API/DTOs/Email/EmailMigrationDto.cs b/API/DTOs/Email/EmailMigrationDto.cs index e7a941405..f051e7337 100644 --- a/API/DTOs/Email/EmailMigrationDto.cs +++ b/API/DTOs/Email/EmailMigrationDto.cs @@ -2,11 +2,11 @@ public class EmailMigrationDto { - public string EmailAddress { get; init; } - public string Username { get; init; } - public string ServerConfirmationLink { get; init; } + public string EmailAddress { get; init; } = default!; + public string Username { get; init; } = default!; + public string ServerConfirmationLink { get; init; } = default!; /// /// InstallId of this Kavita Instance /// - public string InstallId { get; init; } + public string InstallId { get; init; } = default!; } diff --git a/API/DTOs/Email/EmailTestResultDto.cs b/API/DTOs/Email/EmailTestResultDto.cs index a41a6027d..6659e3a45 100644 --- a/API/DTOs/Email/EmailTestResultDto.cs +++ b/API/DTOs/Email/EmailTestResultDto.cs @@ -6,5 +6,5 @@ public class EmailTestResultDto { public bool Successful { get; set; } - public string ErrorMessage { get; set; } + public string ErrorMessage { get; set; } = default!; } diff --git a/API/DTOs/Email/PasswordResetEmailDto.cs b/API/DTOs/Email/PasswordResetEmailDto.cs index 503a9c5e3..06abba171 100644 --- a/API/DTOs/Email/PasswordResetEmailDto.cs +++ b/API/DTOs/Email/PasswordResetEmailDto.cs @@ -2,10 +2,10 @@ public class PasswordResetEmailDto { - public string EmailAddress { get; init; } - public string ServerConfirmationLink { get; init; } + public string EmailAddress { get; init; } = default!; + public string ServerConfirmationLink { get; init; } = default!; /// /// InstallId of this Kavita Instance /// - public string InstallId { get; init; } + public string InstallId { get; init; } = default!; } diff --git a/API/DTOs/Email/SendToDto.cs b/API/DTOs/Email/SendToDto.cs index 254f7fd09..1261d110c 100644 --- a/API/DTOs/Email/SendToDto.cs +++ b/API/DTOs/Email/SendToDto.cs @@ -4,6 +4,6 @@ namespace API.DTOs.Email; public class SendToDto { - public string DestinationEmail { get; set; } - public IEnumerable FilePaths { get; set; } + public string DestinationEmail { get; set; } = default!; + public IEnumerable FilePaths { get; set; } = default!; } diff --git a/API/DTOs/Email/TestEmailDto.cs b/API/DTOs/Email/TestEmailDto.cs index dba9d05f0..37c12ed30 100644 --- a/API/DTOs/Email/TestEmailDto.cs +++ b/API/DTOs/Email/TestEmailDto.cs @@ -2,5 +2,5 @@ public class TestEmailDto { - public string Url { get; set; } + public string Url { get; set; } = default!; } diff --git a/API/DTOs/Filtering/FilterDto.cs b/API/DTOs/Filtering/FilterDto.cs index f6c47f71f..1b8cffc9e 100644 --- a/API/DTOs/Filtering/FilterDto.cs +++ b/API/DTOs/Filtering/FilterDto.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Runtime.InteropServices; using API.Entities; using API.Entities.Enums; @@ -81,7 +80,7 @@ public class FilterDto /// /// Sorting Options for a query. Defaults to null, which uses the queries natural sorting order /// - public SortOptions SortOptions { get; set; } = null; + public SortOptions? SortOptions { get; set; } = null; /// /// Age Ratings. Empty list will return everything back /// @@ -99,10 +98,8 @@ public class FilterDto /// An optional name string to filter by. Empty string will ignore. /// public string SeriesNameQuery { get; init; } = string.Empty; - #nullable enable /// /// An optional release year to filter by. Null will ignore. You can pass 0 for an individual field to ignore it. /// public Range? ReleaseYearRange { get; init; } = null; - #nullable disable } diff --git a/API/DTOs/Filtering/LanguageDto.cs b/API/DTOs/Filtering/LanguageDto.cs index b09aed5d1..bc7ebb5cc 100644 --- a/API/DTOs/Filtering/LanguageDto.cs +++ b/API/DTOs/Filtering/LanguageDto.cs @@ -2,6 +2,6 @@ public class LanguageDto { - public string IsoCode { get; set; } - public string Title { get; set; } + public required string IsoCode { get; set; } + public required string Title { get; set; } } diff --git a/API/DTOs/Filtering/Range.cs b/API/DTOs/Filtering/Range.cs index 383ce7887..b9e9a5e49 100644 --- a/API/DTOs/Filtering/Range.cs +++ b/API/DTOs/Filtering/Range.cs @@ -4,8 +4,8 @@ /// public class Range { - public T Min { get; set; } - public T Max { get; set; } + public T? Min { get; init; } + public T? Max { get; init; } public override string ToString() { diff --git a/API/DTOs/GroupedSeriesDto.cs b/API/DTOs/GroupedSeriesDto.cs index 9795da16e..697ae3a53 100644 --- a/API/DTOs/GroupedSeriesDto.cs +++ b/API/DTOs/GroupedSeriesDto.cs @@ -7,7 +7,7 @@ namespace API.DTOs; /// public class GroupedSeriesDto { - public string SeriesName { get; set; } + public string SeriesName { get; set; } = default!; public int SeriesId { get; set; } public int LibraryId { get; set; } public LibraryType LibraryType { get; set; } diff --git a/API/DTOs/Jobs/JobDto.cs b/API/DTOs/Jobs/JobDto.cs index dc566961e..89d7b30f9 100644 --- a/API/DTOs/Jobs/JobDto.cs +++ b/API/DTOs/Jobs/JobDto.cs @@ -7,11 +7,11 @@ public class JobDto /// /// Job Id /// - public string Id { get; set; } + public string Id { get; set; } = default!; /// /// Human Readable title for the Job /// - public string Title { get; set; } + public string Title { get; set; } = default!; /// /// When the job was created /// @@ -28,5 +28,5 @@ public class JobDto /// Last time the job was run /// public DateTime? LastExecutionUtc { get; set; } - public string Cron { get; set; } + public string Cron { get; set; } = default!; } diff --git a/API/DTOs/JumpBar/JumpKeyDto.cs b/API/DTOs/JumpBar/JumpKeyDto.cs index 44545b65a..5a98a85ca 100644 --- a/API/DTOs/JumpBar/JumpKeyDto.cs +++ b/API/DTOs/JumpBar/JumpKeyDto.cs @@ -9,12 +9,13 @@ public class JumpKeyDto /// Number of items in this Key /// public int Size { get; set; } + /// /// Code to use in URL (url encoded) /// - public string Key { get; set; } + public string Key { get; set; } = default!; /// /// What is visible to user /// - public string Title { get; set; } + public string Title { get; set; } = default!; } diff --git a/API/DTOs/LibraryDto.cs b/API/DTOs/LibraryDto.cs index 5454ce5f5..7696f6fbe 100644 --- a/API/DTOs/LibraryDto.cs +++ b/API/DTOs/LibraryDto.cs @@ -7,7 +7,7 @@ namespace API.DTOs; public class LibraryDto { public int Id { get; init; } - public string Name { get; init; } + public string? Name { get; init; } /// /// Last time Library was scanned /// @@ -16,7 +16,7 @@ public class LibraryDto /// /// An optional Cover Image or null /// - public string CoverImage { get; init; } + public string? CoverImage { get; init; } /// /// If Folder Watching is enabled for this library /// @@ -37,9 +37,9 @@ public class LibraryDto /// Include library series in Search /// public bool IncludeInSearch { get; set; } = true; + public ICollection Folders { get; init; } = new List(); /// /// When showing series, only parent series or series with no relationships will be returned /// public bool CollapseSeriesRelationships { get; set; } = false; - public ICollection Folders { get; init; } } diff --git a/API/DTOs/MangaFileDto.cs b/API/DTOs/MangaFileDto.cs index 7945d1872..9be3c117f 100644 --- a/API/DTOs/MangaFileDto.cs +++ b/API/DTOs/MangaFileDto.cs @@ -6,7 +6,7 @@ namespace API.DTOs; public class MangaFileDto { public int Id { get; init; } - public string FilePath { get; init; } + public string FilePath { get; init; } = default!; public int Pages { get; init; } public long Bytes { get; init; } public MangaFormat Format { get; init; } diff --git a/API/DTOs/MemberDto.cs b/API/DTOs/MemberDto.cs index 1805c1d24..23d87358a 100644 --- a/API/DTOs/MemberDto.cs +++ b/API/DTOs/MemberDto.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using API.Data.Misc; using API.DTOs.Account; -using API.Entities.Enums; namespace API.DTOs; @@ -12,11 +10,11 @@ namespace API.DTOs; public class MemberDto { public int Id { get; init; } - public string Username { get; init; } - public string Email { get; init; } - public AgeRestrictionDto AgeRestriction { get; init; } + public string? Username { get; init; } + public string? Email { get; init; } + public AgeRestrictionDto? AgeRestriction { get; init; } public DateTime Created { get; init; } public DateTime LastActive { get; init; } - public IEnumerable Libraries { get; init; } - public IEnumerable Roles { get; init; } + public IEnumerable? Libraries { get; init; } + public IEnumerable? Roles { get; init; } } diff --git a/API/DTOs/Metadata/AgeRatingDto.cs b/API/DTOs/Metadata/AgeRatingDto.cs index cbeb44e33..07523c3fe 100644 --- a/API/DTOs/Metadata/AgeRatingDto.cs +++ b/API/DTOs/Metadata/AgeRatingDto.cs @@ -5,5 +5,5 @@ namespace API.DTOs.Metadata; public class AgeRatingDto { public AgeRating Value { get; set; } - public string Title { get; set; } + public required string Title { get; set; } } diff --git a/API/DTOs/Metadata/ChapterMetadataDto.cs b/API/DTOs/Metadata/ChapterMetadataDto.cs index cea8638d3..b9b04cfac 100644 --- a/API/DTOs/Metadata/ChapterMetadataDto.cs +++ b/API/DTOs/Metadata/ChapterMetadataDto.cs @@ -10,7 +10,7 @@ public class ChapterMetadataDto { public int Id { get; set; } public int ChapterId { get; set; } - public string Title { get; set; } + public string Title { get; set; } = default!; public ICollection Writers { get; set; } = new List(); public ICollection CoverArtists { get; set; } = new List(); public ICollection Publishers { get; set; } = new List(); @@ -29,16 +29,16 @@ public class ChapterMetadataDto /// public ICollection Tags { get; set; } = new List(); public AgeRating AgeRating { get; set; } - public string ReleaseDate { get; set; } + public string? ReleaseDate { get; set; } public PublicationStatus PublicationStatus { get; set; } /// /// Summary for the Chapter/Issue /// - public string Summary { get; set; } + public string? Summary { get; set; } /// /// Language for the Chapter/Issue /// - public string Language { get; set; } + public string? Language { get; set; } /// /// Number in the TotalCount of issues /// diff --git a/API/DTOs/Metadata/GenreTagDto.cs b/API/DTOs/Metadata/GenreTagDto.cs index 21d02273d..cf05ebbff 100644 --- a/API/DTOs/Metadata/GenreTagDto.cs +++ b/API/DTOs/Metadata/GenreTagDto.cs @@ -3,5 +3,5 @@ public class GenreTagDto { public int Id { get; set; } - public string Title { get; set; } + public required string Title { get; set; } } diff --git a/API/DTOs/Metadata/PublicationStatusDto.cs b/API/DTOs/Metadata/PublicationStatusDto.cs index 332223428..b8166a6e5 100644 --- a/API/DTOs/Metadata/PublicationStatusDto.cs +++ b/API/DTOs/Metadata/PublicationStatusDto.cs @@ -5,5 +5,5 @@ namespace API.DTOs.Metadata; public class PublicationStatusDto { public PublicationStatus Value { get; set; } - public string Title { get; set; } + public required string Title { get; set; } } diff --git a/API/DTOs/Metadata/TagDto.cs b/API/DTOs/Metadata/TagDto.cs index 6e9b2f71e..59e03a279 100644 --- a/API/DTOs/Metadata/TagDto.cs +++ b/API/DTOs/Metadata/TagDto.cs @@ -3,5 +3,5 @@ public class TagDto { public int Id { get; set; } - public string Title { get; set; } + public required string Title { get; set; } } diff --git a/API/DTOs/OPDS/FeedCategory.cs b/API/DTOs/OPDS/FeedCategory.cs index 71684a257..3129fab60 100644 --- a/API/DTOs/OPDS/FeedCategory.cs +++ b/API/DTOs/OPDS/FeedCategory.cs @@ -8,11 +8,11 @@ public class FeedCategory public string Scheme { get; } = "http://www.bisg.org/standards/bisac_subject/index.html"; [XmlAttribute("term")] - public string Term { get; set; } + public string Term { get; set; } = default!; /// /// The actual genre /// [XmlAttribute("label")] - public string Label { get; set; } + public string Label { get; set; } = default!; } diff --git a/API/DTOs/OPDS/FeedEntry.cs b/API/DTOs/OPDS/FeedEntry.cs index 61594278c..e2210e2e8 100644 --- a/API/DTOs/OPDS/FeedEntry.cs +++ b/API/DTOs/OPDS/FeedEntry.cs @@ -10,13 +10,13 @@ public class FeedEntry public string Updated { get; init; } = DateTime.UtcNow.ToString("s"); [XmlElement("id")] - public string Id { get; set; } + public required string Id { get; set; } [XmlElement("title")] - public string Title { get; set; } + public required string Title { get; set; } [XmlElement("summary")] - public string Summary { get; set; } + public string? Summary { get; set; } /// /// Represents Size of the Entry @@ -24,20 +24,20 @@ public class FeedEntry /// 2 MB /// [XmlElement("extent", Namespace = "http://purl.org/dc/terms/")] - public string Extent { get; set; } + public string? Extent { get; set; } /// /// Format of the file /// https://dublincore.org/specifications/dublin-core/dcmi-terms/ /// [XmlElement("format", Namespace = "http://purl.org/dc/terms/format")] - public string Format { get; set; } + public string? Format { get; set; } [XmlElement("language", Namespace = "http://purl.org/dc/terms/")] - public string Language { get; set; } + public string? Language { get; set; } [XmlElement("content")] - public FeedEntryContent Content { get; set; } + public FeedEntryContent? Content { get; set; } [XmlElement("link")] public List Links { get; set; } = new List(); diff --git a/API/DTOs/OPDS/OpenSearchDescription.cs b/API/DTOs/OPDS/OpenSearchDescription.cs index 6ee043ac4..cc8392a88 100644 --- a/API/DTOs/OPDS/OpenSearchDescription.cs +++ b/API/DTOs/OPDS/OpenSearchDescription.cs @@ -8,29 +8,29 @@ public class OpenSearchDescription /// /// Contains a brief human-readable title that identifies this search engine. /// - public string ShortName { get; set; } + public string ShortName { get; set; } = default!; /// /// Contains an extended human-readable title that identifies this search engine. /// - public string LongName { get; set; } + public string LongName { get; set; } = default!; /// /// Contains a human-readable text description of the search engine. /// - public string Description { get; set; } + public string Description { get; set; } = default!; /// /// https://github.com/dewitt/opensearch/blob/master/opensearch-1-1-draft-6.md#the-url-element /// - public SearchLink Url { get; set; } + public SearchLink Url { get; set; } = default!; /// /// Contains a set of words that are used as keywords to identify and categorize this search content. /// Tags must be a single word and are delimited by the space character (' '). /// - public string Tags { get; set; } + public string Tags { get; set; } = string.Empty; /// /// Contains a URL that identifies the location of an image that can be used in association with this search content. /// http://example.com/websearch.png /// - public string Image { get; set; } + public string Image { get; set; } = default!; public string InputEncoding { get; set; } = "UTF-8"; public string OutputEncoding { get; set; } = "UTF-8"; /// diff --git a/API/DTOs/OPDS/SearchLink.cs b/API/DTOs/OPDS/SearchLink.cs index 6aeca506a..dba67f3bd 100644 --- a/API/DTOs/OPDS/SearchLink.cs +++ b/API/DTOs/OPDS/SearchLink.cs @@ -5,11 +5,11 @@ namespace API.DTOs.OPDS; public class SearchLink { [XmlAttribute("type")] - public string Type { get; set; } + public string Type { get; set; } = default!; [XmlAttribute("rel")] public string Rel { get; set; } = "results"; [XmlAttribute("template")] - public string Template { get; set; } + public string Template { get; set; } = default!; } diff --git a/API/DTOs/PersonDto.cs b/API/DTOs/PersonDto.cs index 92bd81924..85cc72bb0 100644 --- a/API/DTOs/PersonDto.cs +++ b/API/DTOs/PersonDto.cs @@ -5,6 +5,6 @@ namespace API.DTOs; public class PersonDto { public int Id { get; set; } - public string Name { get; set; } + public required string Name { get; set; } public PersonRole Role { get; set; } } diff --git a/API/DTOs/ProgressDto.cs b/API/DTOs/ProgressDto.cs index e5f796bcc..3356a4827 100644 --- a/API/DTOs/ProgressDto.cs +++ b/API/DTOs/ProgressDto.cs @@ -1,5 +1,4 @@ -using System; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace API.DTOs; @@ -19,5 +18,5 @@ public class ProgressDto /// For EPUB reader, this can be an optional string of the id of a part marker, to help resume reading position /// on pages that combine multiple "chapters". /// - public string BookScrollId { get; set; } + public string? BookScrollId { get; set; } } diff --git a/API/DTOs/Reader/BookChapterItem.cs b/API/DTOs/Reader/BookChapterItem.cs index 3dabbd1ec..dcfb7b904 100644 --- a/API/DTOs/Reader/BookChapterItem.cs +++ b/API/DTOs/Reader/BookChapterItem.cs @@ -7,14 +7,14 @@ public class BookChapterItem /// /// Name of the Chapter /// - public string Title { get; set; } + public string Title { get; set; } = default!; /// /// A part represents the id of the anchor so we can scroll to it. 01_values.xhtml#h_sVZPaxUSy/ /// - public string Part { get; set; } + public string Part { get; set; } = default!; /// /// Page Number to load for the chapter /// public int Page { get; set; } - public ICollection Children { get; set; } + public ICollection Children { get; set; } = default!; } diff --git a/API/DTOs/Reader/BookInfoDto.cs b/API/DTOs/Reader/BookInfoDto.cs index 78cfc39b0..c379f71f8 100644 --- a/API/DTOs/Reader/BookInfoDto.cs +++ b/API/DTOs/Reader/BookInfoDto.cs @@ -4,15 +4,15 @@ namespace API.DTOs.Reader; public class BookInfoDto : IChapterInfoDto { - public string BookTitle { get; set; } + public string BookTitle { get; set; } = default! ; public int SeriesId { get; set; } public int VolumeId { get; set; } public MangaFormat SeriesFormat { get; set; } - public string SeriesName { get; set; } - public string ChapterNumber { get; set; } - public string VolumeNumber { get; set; } + public string SeriesName { get; set; } = default! ; + public string ChapterNumber { get; set; } = default! ; + public string VolumeNumber { get; set; } = default! ; public int LibraryId { get; set; } public int Pages { get; set; } public bool IsSpecial { get; set; } - public string ChapterTitle { get; set; } + public string ChapterTitle { get; set; } = default! ; } diff --git a/API/DTOs/Reader/BookmarkInfoDto.cs b/API/DTOs/Reader/BookmarkInfoDto.cs index a34eb81c2..57bfa989c 100644 --- a/API/DTOs/Reader/BookmarkInfoDto.cs +++ b/API/DTOs/Reader/BookmarkInfoDto.cs @@ -4,7 +4,7 @@ namespace API.DTOs.Reader; public class BookmarkInfoDto { - public string SeriesName { get; set; } + public string SeriesName { get; set; } = default!; public MangaFormat SeriesFormat { get; set; } public int SeriesId { get; set; } public int LibraryId { get; set; } diff --git a/API/DTOs/Reader/BulkRemoveBookmarkForSeriesDto.cs b/API/DTOs/Reader/BulkRemoveBookmarkForSeriesDto.cs index 9cd22f958..7490f837c 100644 --- a/API/DTOs/Reader/BulkRemoveBookmarkForSeriesDto.cs +++ b/API/DTOs/Reader/BulkRemoveBookmarkForSeriesDto.cs @@ -4,5 +4,5 @@ namespace API.DTOs.Reader; public class BulkRemoveBookmarkForSeriesDto { - public ICollection SeriesIds { get; init; } + public ICollection SeriesIds { get; init; } = default!; } diff --git a/API/DTOs/Reader/ChapterInfoDto.cs b/API/DTOs/Reader/ChapterInfoDto.cs index 5ea7be7fd..36ddd554e 100644 --- a/API/DTOs/Reader/ChapterInfoDto.cs +++ b/API/DTOs/Reader/ChapterInfoDto.cs @@ -11,11 +11,11 @@ public class ChapterInfoDto : IChapterInfoDto /// /// The Chapter Number /// - public string ChapterNumber { get; set; } + public string ChapterNumber { get; set; } = default! ; /// /// The Volume Number /// - public string VolumeNumber { get; set; } + public string VolumeNumber { get; set; } = default! ; /// /// Volume entity Id /// @@ -23,7 +23,7 @@ public class ChapterInfoDto : IChapterInfoDto /// /// Series Name /// - public string SeriesName { get; set; } + public string SeriesName { get; set; } = null!; /// /// Series Format /// @@ -51,7 +51,7 @@ public class ChapterInfoDto : IChapterInfoDto /// /// File name of the chapter /// - public string FileName { get; set; } + public string? FileName { get; set; } /// /// If this is marked as a special in Kavita /// @@ -59,21 +59,22 @@ public class ChapterInfoDto : IChapterInfoDto /// /// The subtitle to render on the reader /// - public string Subtitle { get; set; } + public string? Subtitle { get; set; } /// /// Series Title /// /// Usually just series name, but can include chapter title - public string Title { get; set; } + public string Title { get; set; } = default!; + /// /// List of all files with their inner archive structure maintained in filename and dimensions /// /// This is optionally returned by includeDimensions - public IEnumerable PageDimensions { get; set; } + public IEnumerable? PageDimensions { get; set; } /// /// For Double Page reader, this will contain snap points to ensure the reader always resumes on correct page /// /// This is optionally returned by includeDimensions - public IDictionary DoublePairs { get; set; } + public IDictionary? DoublePairs { get; set; } } diff --git a/API/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs b/API/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs index da36e44f5..50187ec81 100644 --- a/API/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs +++ b/API/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs @@ -4,5 +4,5 @@ namespace API.DTOs.Reader; public class MarkMultipleSeriesAsReadDto { - public IReadOnlyList SeriesIds { get; init; } + public IReadOnlyList SeriesIds { get; init; } = default!; } diff --git a/API/DTOs/Reader/MarkVolumesReadDto.cs b/API/DTOs/Reader/MarkVolumesReadDto.cs index 9f02af524..ebe1cd76c 100644 --- a/API/DTOs/Reader/MarkVolumesReadDto.cs +++ b/API/DTOs/Reader/MarkVolumesReadDto.cs @@ -11,9 +11,9 @@ public class MarkVolumesReadDto /// /// A list of Volumes to mark read /// - public IReadOnlyList VolumeIds { get; set; } + public IReadOnlyList VolumeIds { get; set; } = default!; /// /// A list of additional Chapters to mark as read /// - public IReadOnlyList ChapterIds { get; set; } + public IReadOnlyList ChapterIds { get; set; } = default!; } diff --git a/API/DTOs/ReadingLists/CreateReadingListDto.cs b/API/DTOs/ReadingLists/CreateReadingListDto.cs index 396c05e7c..783253007 100644 --- a/API/DTOs/ReadingLists/CreateReadingListDto.cs +++ b/API/DTOs/ReadingLists/CreateReadingListDto.cs @@ -2,5 +2,5 @@ public class CreateReadingListDto { - public string Title { get; init; } + public string Title { get; init; } = default!; } diff --git a/API/DTOs/ReadingLists/ReadingListDto.cs b/API/DTOs/ReadingLists/ReadingListDto.cs index de212217e..9e51c4310 100644 --- a/API/DTOs/ReadingLists/ReadingListDto.cs +++ b/API/DTOs/ReadingLists/ReadingListDto.cs @@ -3,8 +3,8 @@ public class ReadingListDto { public int Id { get; init; } - public string Title { get; set; } - public string Summary { get; set; } + public string Title { get; set; } = default!; + public string Summary { get; set; } = default!; /// /// Reading lists that are promoted are only done by admins /// diff --git a/API/DTOs/ReadingLists/ReadingListItemDto.cs b/API/DTOs/ReadingLists/ReadingListItemDto.cs index 79fdd9d8f..6bcc462b7 100644 --- a/API/DTOs/ReadingLists/ReadingListItemDto.cs +++ b/API/DTOs/ReadingLists/ReadingListItemDto.cs @@ -9,18 +9,18 @@ public class ReadingListItemDto public int Order { get; init; } public int ChapterId { get; init; } public int SeriesId { get; init; } - public string SeriesName { get; set; } + public string? SeriesName { get; set; } public MangaFormat SeriesFormat { get; set; } public int PagesRead { get; set; } public int PagesTotal { get; set; } - public string ChapterNumber { get; set; } - public string ChapterTitleName { get; set; } - public string VolumeNumber { get; set; } + public string? ChapterNumber { get; set; } + public string? VolumeNumber { get; set; } + public string? ChapterTitleName { get; set; } public int VolumeId { get; set; } public int LibraryId { get; set; } + public string? Title { get; set; } public LibraryType LibraryType { get; set; } - public string LibraryName { get; set; } - public string Title { get; set; } + public string? LibraryName { get; set; } /// /// Release Date from Chapter /// diff --git a/API/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs b/API/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs index 0d4bfb0dd..408963529 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs +++ b/API/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs @@ -6,6 +6,6 @@ public class UpdateReadingListByMultipleDto { public int SeriesId { get; init; } public int ReadingListId { get; init; } - public IReadOnlyList VolumeIds { get; init; } - public IReadOnlyList ChapterIds { get; init; } + public IReadOnlyList VolumeIds { get; init; } = default!; + public IReadOnlyList ChapterIds { get; init; } = default!; } diff --git a/API/DTOs/ReadingLists/UpdateReadingListByMultipleSeriesDto.cs b/API/DTOs/ReadingLists/UpdateReadingListByMultipleSeriesDto.cs index 944d4ff78..f910e9c06 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListByMultipleSeriesDto.cs +++ b/API/DTOs/ReadingLists/UpdateReadingListByMultipleSeriesDto.cs @@ -5,5 +5,5 @@ namespace API.DTOs.ReadingLists; public class UpdateReadingListByMultipleSeriesDto { public int ReadingListId { get; init; } - public IReadOnlyList SeriesIds { get; init; } + public IReadOnlyList SeriesIds { get; init; } = default!; } diff --git a/API/DTOs/ReadingLists/UpdateReadingListDto.cs b/API/DTOs/ReadingLists/UpdateReadingListDto.cs index 6be7b8f69..ca93947c2 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListDto.cs +++ b/API/DTOs/ReadingLists/UpdateReadingListDto.cs @@ -1,5 +1,4 @@ -using System; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace API.DTOs.ReadingLists; diff --git a/API/DTOs/RecentlyAddedItemDto.cs b/API/DTOs/RecentlyAddedItemDto.cs index 6c7df8b4d..93ef9ac9a 100644 --- a/API/DTOs/RecentlyAddedItemDto.cs +++ b/API/DTOs/RecentlyAddedItemDto.cs @@ -8,14 +8,14 @@ namespace API.DTOs; /// public class RecentlyAddedItemDto { - public string SeriesName { get; set; } + public string SeriesName { get; set; } = default!; public int SeriesId { get; set; } public int LibraryId { get; set; } public LibraryType LibraryType { get; set; } /// /// This will automatically map to Volume X, Chapter Y, etc. /// - public string Title { get; set; } + public string Title { get; set; } = default!; public DateTime Created { get; set; } /// /// Chapter Id if this is a chapter. Not guaranteed to be set. diff --git a/API/DTOs/RegisterDto.cs b/API/DTOs/RegisterDto.cs index 4e542f1c0..b6132046f 100644 --- a/API/DTOs/RegisterDto.cs +++ b/API/DTOs/RegisterDto.cs @@ -5,12 +5,12 @@ namespace API.DTOs; public class RegisterDto { [Required] - public string Username { get; init; } + public string Username { get; init; } = default!; /// /// An email to register with. Optional. Provides Forgot Password functionality /// - public string Email { get; init; } + public string Email { get; init; } = default!; [Required] [StringLength(32, MinimumLength = 6)] - public string Password { get; set; } + public string Password { get; set; } = default!; } diff --git a/API/DTOs/ScanFolderDto.cs b/API/DTOs/ScanFolderDto.cs index 59ce4d0b5..684de909e 100644 --- a/API/DTOs/ScanFolderDto.cs +++ b/API/DTOs/ScanFolderDto.cs @@ -8,10 +8,10 @@ public class ScanFolderDto /// /// Api key for a user with Admin permissions /// - public string ApiKey { get; set; } + public string ApiKey { get; set; } = default!; /// /// Folder Path to Scan /// /// JSON cannot accept /, so you may need to use // escaping on paths - public string FolderPath { get; set; } + public string FolderPath { get; set; } = default!; } diff --git a/API/DTOs/Search/SearchResultDto.cs b/API/DTOs/Search/SearchResultDto.cs index 4d9e300a5..6fcae3b5d 100644 --- a/API/DTOs/Search/SearchResultDto.cs +++ b/API/DTOs/Search/SearchResultDto.cs @@ -5,13 +5,13 @@ namespace API.DTOs.Search; public class SearchResultDto { public int SeriesId { get; init; } - public string Name { get; init; } - public string OriginalName { get; init; } - public string SortName { get; init; } - public string LocalizedName { get; init; } + public string Name { get; init; } = default!; + public string OriginalName { get; init; } = default!; + public string SortName { get; init; } = default!; + public string LocalizedName { get; init; } = default!; public MangaFormat Format { get; init; } // Grouping information - public string LibraryName { get; set; } + public string LibraryName { get; set; } = default!; public int LibraryId { get; set; } } diff --git a/API/DTOs/Search/SearchResultGroupDto.cs b/API/DTOs/Search/SearchResultGroupDto.cs index 0a1fac402..66370fb0a 100644 --- a/API/DTOs/Search/SearchResultGroupDto.cs +++ b/API/DTOs/Search/SearchResultGroupDto.cs @@ -10,15 +10,15 @@ namespace API.DTOs.Search; /// public class SearchResultGroupDto { - public IEnumerable Libraries { get; set; } - public IEnumerable Series { get; set; } - public IEnumerable Collections { get; set; } - public IEnumerable ReadingLists { get; set; } - public IEnumerable Persons { get; set; } - public IEnumerable Genres { get; set; } - public IEnumerable Tags { get; set; } - public IEnumerable Files { get; set; } - public IEnumerable Chapters { get; set; } + public IEnumerable Libraries { get; set; } = default!; + public IEnumerable Series { get; set; } = default!; + public IEnumerable Collections { get; set; } = default!; + public IEnumerable ReadingLists { get; set; } = default!; + public IEnumerable Persons { get; set; } = default!; + public IEnumerable Genres { get; set; } = default!; + public IEnumerable Tags { get; set; } = default!; + public IEnumerable Files { get; set; } = default!; + public IEnumerable Chapters { get; set; } = default!; } diff --git a/API/DTOs/SeriesByIdsDto.cs b/API/DTOs/SeriesByIdsDto.cs index 29c028156..12e13d96f 100644 --- a/API/DTOs/SeriesByIdsDto.cs +++ b/API/DTOs/SeriesByIdsDto.cs @@ -2,5 +2,5 @@ public class SeriesByIdsDto { - public int[] SeriesIds { get; init; } + public int[] SeriesIds { get; init; } = default!; } diff --git a/API/DTOs/SeriesDetail/RelatedSeriesDto.cs b/API/DTOs/SeriesDetail/RelatedSeriesDto.cs index 452da9cf5..72271ff73 100644 --- a/API/DTOs/SeriesDetail/RelatedSeriesDto.cs +++ b/API/DTOs/SeriesDetail/RelatedSeriesDto.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using API.Entities.Enums; namespace API.DTOs.SeriesDetail; @@ -10,17 +9,17 @@ public class RelatedSeriesDto /// public int SourceSeriesId { get; set; } - public IEnumerable Sequels { get; set; } - public IEnumerable Prequels { get; set; } - public IEnumerable SpinOffs { get; set; } - public IEnumerable Adaptations { get; set; } - public IEnumerable SideStories { get; set; } - public IEnumerable Characters { get; set; } - public IEnumerable Contains { get; set; } - public IEnumerable Others { get; set; } - public IEnumerable AlternativeSettings { get; set; } - public IEnumerable AlternativeVersions { get; set; } - public IEnumerable Doujinshis { get; set; } - public IEnumerable Parent { get; set; } - public IEnumerable Editions { get; set; } + public IEnumerable Sequels { get; set; } = default!; + public IEnumerable Prequels { get; set; } = default!; + public IEnumerable SpinOffs { get; set; } = default!; + public IEnumerable Adaptations { get; set; } = default!; + public IEnumerable SideStories { get; set; } = default!; + public IEnumerable Characters { get; set; } = default!; + public IEnumerable Contains { get; set; } = default!; + public IEnumerable Others { get; set; } = default!; + public IEnumerable AlternativeSettings { get; set; } = default!; + public IEnumerable AlternativeVersions { get; set; } = default!; + public IEnumerable Doujinshis { get; set; } = default!; + public IEnumerable Parent { get; set; } = default!; + public IEnumerable Editions { get; set; } = default!; } diff --git a/API/DTOs/SeriesDetail/SeriesDetailDto.cs b/API/DTOs/SeriesDetail/SeriesDetailDto.cs index 2438755c6..9fc067803 100644 --- a/API/DTOs/SeriesDetail/SeriesDetailDto.cs +++ b/API/DTOs/SeriesDetail/SeriesDetailDto.cs @@ -11,19 +11,19 @@ public class SeriesDetailDto /// /// Specials for the Series. These will have their title and range cleaned to remove the special marker and prepare /// - public IEnumerable Specials { get; set; } + public IEnumerable Specials { get; set; } = default!; /// /// All Chapters, excluding Specials and single chapters (0 chapter) for a volume /// - public IEnumerable Chapters { get; set; } + public IEnumerable Chapters { get; set; } = default!; /// /// Just the Volumes for the Series (Excludes Volume 0) /// - public IEnumerable Volumes { get; set; } + public IEnumerable Volumes { get; set; } = default!; /// /// These are chapters that are in Volume 0 and should be read AFTER the volumes /// - public IEnumerable StorylineChapters { get; set; } + public IEnumerable StorylineChapters { get; set; } = default!; /// /// How many chapters are unread /// diff --git a/API/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs b/API/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs index d6976a05d..8a81f766e 100644 --- a/API/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs +++ b/API/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs @@ -5,16 +5,16 @@ namespace API.DTOs.SeriesDetail; public class UpdateRelatedSeriesDto { public int SeriesId { get; set; } - public IList Adaptations { get; set; } - public IList Characters { get; set; } - public IList Contains { get; set; } - public IList Others { get; set; } - public IList Prequels { get; set; } - public IList Sequels { get; set; } - public IList SideStories { get; set; } - public IList SpinOffs { get; set; } - public IList AlternativeSettings { get; set; } - public IList AlternativeVersions { get; set; } - public IList Doujinshis { get; set; } - public IList Editions { get; set; } + public IList Adaptations { get; set; } = default!; + public IList Characters { get; set; } = default!; + public IList Contains { get; set; } = default!; + public IList Others { get; set; } = default!; + public IList Prequels { get; set; } = default!; + public IList Sequels { get; set; } = default!; + public IList SideStories { get; set; } = default!; + public IList SpinOffs { get; set; } = default!; + public IList AlternativeSettings { get; set; } = default!; + public IList AlternativeVersions { get; set; } = default!; + public IList Doujinshis { get; set; } = default!; + public IList Editions { get; set; } = default!; } diff --git a/API/DTOs/SeriesDto.cs b/API/DTOs/SeriesDto.cs index b1b5a9f35..8ebd13303 100644 --- a/API/DTOs/SeriesDto.cs +++ b/API/DTOs/SeriesDto.cs @@ -7,11 +7,11 @@ namespace API.DTOs; public class SeriesDto : IHasReadTimeEstimate { public int Id { get; init; } - public string Name { get; init; } - public string OriginalName { get; init; } - public string LocalizedName { get; init; } - public string SortName { get; init; } - public string Summary { get; init; } + public string? Name { get; init; } + public string? OriginalName { get; init; } + public string? LocalizedName { get; init; } + public string? SortName { get; init; } + public string? Summary { get; init; } public int Pages { get; init; } public bool CoverImageLocked { get; set; } /// @@ -33,7 +33,7 @@ public class SeriesDto : IHasReadTimeEstimate /// /// Review from logged in user. Calculated at API-time. /// - public string UserReview { get; set; } + public string? UserReview { get; set; } public MangaFormat Format { get; set; } public DateTime Created { get; set; } @@ -47,7 +47,7 @@ public class SeriesDto : IHasReadTimeEstimate public long WordCount { get; set; } public int LibraryId { get; set; } - public string LibraryName { get; set; } + public string LibraryName { get; set; } = default!; /// public int MinHoursToRead { get; set; } /// @@ -57,7 +57,7 @@ public class SeriesDto : IHasReadTimeEstimate /// /// The highest level folder for this Series /// - public string FolderPath { get; set; } + public string FolderPath { get; set; } = default!; /// /// The last time the folder for this series was scanned /// diff --git a/API/DTOs/SeriesMetadataDto.cs b/API/DTOs/SeriesMetadataDto.cs index 8853fdb0b..441f58d36 100644 --- a/API/DTOs/SeriesMetadataDto.cs +++ b/API/DTOs/SeriesMetadataDto.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using API.DTOs.CollectionTags; using API.DTOs.Metadata; using API.Entities.Enums; @@ -10,18 +9,21 @@ public class SeriesMetadataDto { public int Id { get; set; } public string Summary { get; set; } = string.Empty; + /// /// Collections the Series belongs to /// - public ICollection CollectionTags { get; set; } + public ICollection CollectionTags { get; set; } = new List(); + /// /// Genres for the Series /// - public ICollection Genres { get; set; } + public ICollection Genres { get; set; } = new List(); + /// /// Collection of all Tags from underlying chapters for a Series /// - public ICollection Tags { get; set; } + public ICollection Tags { get; set; } = new List(); public ICollection Writers { get; set; } = new List(); public ICollection CoverArtists { get; set; } = new List(); public ICollection Publishers { get; set; } = new List(); diff --git a/API/DTOs/Settings/ServerSettingDTO.cs b/API/DTOs/Settings/ServerSettingDTO.cs index 07aa08ce6..90a0901b3 100644 --- a/API/DTOs/Settings/ServerSettingDTO.cs +++ b/API/DTOs/Settings/ServerSettingDTO.cs @@ -1,18 +1,16 @@ -using System; -using System.ComponentModel.DataAnnotations; -using API.Services; +using API.Services; namespace API.DTOs.Settings; public class ServerSettingDto { - public string CacheDirectory { get; set; } - public string TaskScan { get; set; } + public string CacheDirectory { get; set; } = default!; + public string TaskScan { get; set; } = default!; /// /// Logging level for server. Managed in appsettings.json. /// - public string LoggingLevel { get; set; } - public string TaskBackup { get; set; } + public string LoggingLevel { get; set; } = default!; + public string TaskBackup { get; set; } = default!; /// /// Port the server listens on. Managed in appsettings.json. /// @@ -32,22 +30,22 @@ public class ServerSettingDto /// /// Base Url for the kavita. Requires restart to take effect. /// - public string BaseUrl { get; set; } + public string BaseUrl { get; set; } = default!; /// /// Where Bookmarks are stored. /// /// If null or empty string, will default back to default install setting aka - public string BookmarksDirectory { get; set; } + public string BookmarksDirectory { get; set; } = default!; /// /// Email service to use for the invite user flow, forgot password, etc. /// /// If null or empty string, will default back to default install setting aka - public string EmailServiceUrl { get; set; } - public string InstallVersion { get; set; } + public string EmailServiceUrl { get; set; } = default!; + public string InstallVersion { get; set; } = default!; /// /// Represents a unique Id to this Kavita installation. Only used in Stats to identify unique installs. /// - public string InstallId { get; set; } + public string InstallId { get; set; } = default!; /// /// If the server should save bookmarks as WebP encoding /// diff --git a/API/DTOs/Statistics/Count.cs b/API/DTOs/Statistics/Count.cs index dc9803a6f..411b44897 100644 --- a/API/DTOs/Statistics/Count.cs +++ b/API/DTOs/Statistics/Count.cs @@ -2,6 +2,6 @@ public class StatCount : ICount { - public T Value { get; set; } + public T Value { get; set; } = default!; public long Count { get; set; } } diff --git a/API/DTOs/Statistics/FileExtensionBreakdownDto.cs b/API/DTOs/Statistics/FileExtensionBreakdownDto.cs index 66e5f821b..c0d65fe7f 100644 --- a/API/DTOs/Statistics/FileExtensionBreakdownDto.cs +++ b/API/DTOs/Statistics/FileExtensionBreakdownDto.cs @@ -5,7 +5,7 @@ namespace API.DTOs.Statistics; public class FileExtensionDto { - public string Extension { get; set; } + public string? Extension { get; set; } public MangaFormat Format { get; set; } public long TotalSize { get; set; } public long TotalFiles { get; set; } @@ -17,6 +17,7 @@ public class FileExtensionBreakdownDto /// Total bytes for all files /// public long TotalFileSize { get; set; } - public IList FileBreakdown { get; set; } + + public IList FileBreakdown { get; set; } = default!; } diff --git a/API/DTOs/Statistics/PagesReadOnADayCount.cs b/API/DTOs/Statistics/PagesReadOnADayCount.cs index c38a775c7..b1a6bb1ea 100644 --- a/API/DTOs/Statistics/PagesReadOnADayCount.cs +++ b/API/DTOs/Statistics/PagesReadOnADayCount.cs @@ -1,5 +1,4 @@ -using System; -using API.Entities.Enums; +using API.Entities.Enums; namespace API.DTOs.Statistics; @@ -8,7 +7,7 @@ public class PagesReadOnADayCount : ICount /// /// The day of the readings /// - public T Value { get; set; } + public T Value { get; set; } = default!; /// /// Number of pages read /// diff --git a/API/DTOs/Statistics/ReadHistoryEvent.cs b/API/DTOs/Statistics/ReadHistoryEvent.cs index 72377c823..9e32aa792 100644 --- a/API/DTOs/Statistics/ReadHistoryEvent.cs +++ b/API/DTOs/Statistics/ReadHistoryEvent.cs @@ -8,11 +8,11 @@ namespace API.DTOs.Statistics; public class ReadHistoryEvent { public int UserId { get; set; } - public string UserName { get; set; } + public required string? UserName { get; set; } = default!; public int LibraryId { get; set; } public int SeriesId { get; set; } - public string SeriesName { get; set; } + public required string SeriesName { get; set; } = default!; public DateTime ReadDate { get; set; } public int ChapterId { get; set; } - public string ChapterNumber { get; set; } + public required string ChapterNumber { get; set; } = default!; } diff --git a/API/DTOs/Statistics/ServerStatisticsDto.cs b/API/DTOs/Statistics/ServerStatisticsDto.cs index d727e3227..059b25204 100644 --- a/API/DTOs/Statistics/ServerStatisticsDto.cs +++ b/API/DTOs/Statistics/ServerStatisticsDto.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace API.DTOs.Statistics; @@ -14,17 +13,17 @@ public class ServerStatisticsDto public long TotalTags { get; set; } public long TotalPeople { get; set; } public long TotalReadingTime { get; set; } - public IEnumerable> MostReadSeries { get; set; } + public IEnumerable>? MostReadSeries { get; set; } /// /// Total users who have started/reading/read per series /// - public IEnumerable> MostPopularSeries { get; set; } - public IEnumerable> MostActiveUsers { get; set; } - public IEnumerable> MostActiveLibraries { get; set; } + public IEnumerable>? MostPopularSeries { get; set; } + public IEnumerable>? MostActiveUsers { get; set; } + public IEnumerable>? MostActiveLibraries { get; set; } /// /// Last 5 Series read /// - public IEnumerable RecentlyRead { get; set; } + public IEnumerable? RecentlyRead { get; set; } } diff --git a/API/DTOs/Statistics/TopReadsDto.cs b/API/DTOs/Statistics/TopReadsDto.cs index dbcb718dc..819e55ad5 100644 --- a/API/DTOs/Statistics/TopReadsDto.cs +++ b/API/DTOs/Statistics/TopReadsDto.cs @@ -1,11 +1,9 @@ -using System.Collections.Generic; - -namespace API.DTOs.Statistics; +namespace API.DTOs.Statistics; public class TopReadDto { public int UserId { get; set; } - public string Username { get; set; } + public string? Username { get; set; } = default!; /// /// Amount of time read on Comic libraries /// diff --git a/API/DTOs/Statistics/UserReadStatistics.cs b/API/DTOs/Statistics/UserReadStatistics.cs index 686957e93..5e3f5aa5d 100644 --- a/API/DTOs/Statistics/UserReadStatistics.cs +++ b/API/DTOs/Statistics/UserReadStatistics.cs @@ -20,6 +20,6 @@ public class UserReadStatistics public long ChaptersRead { get; set; } public DateTime LastActive { get; set; } public double AvgHoursPerWeekSpentReading { get; set; } - public IEnumerable> PercentReadPerLibrary { get; set; } + public IEnumerable>? PercentReadPerLibrary { get; set; } } diff --git a/API/DTOs/Stats/FileFormatDto.cs b/API/DTOs/Stats/FileFormatDto.cs index 67385e746..6319bd2a9 100644 --- a/API/DTOs/Stats/FileFormatDto.cs +++ b/API/DTOs/Stats/FileFormatDto.cs @@ -7,9 +7,9 @@ public class FileFormatDto /// /// The extension with the ., in lowercase /// - public string Extension { get; set; } + public required string Extension { get; set; } /// /// Format of extension /// - public MangaFormat Format { get; set; } + public required MangaFormat Format { get; set; } } diff --git a/API/DTOs/Stats/ServerInfoDto.cs b/API/DTOs/Stats/ServerInfoDto.cs index 629b3da6d..d8c60920e 100644 --- a/API/DTOs/Stats/ServerInfoDto.cs +++ b/API/DTOs/Stats/ServerInfoDto.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using API.Entities.Enums; -using Microsoft.AspNetCore.Mvc.RazorPages; namespace API.DTOs.Stats; @@ -12,8 +11,8 @@ public class ServerInfoDto /// /// Unique Id that represents a unique install /// - public string InstallId { get; set; } - public string Os { get; set; } + public required string InstallId { get; set; } + public required string Os { get; set; } /// /// If the Kavita install is using Docker /// @@ -21,11 +20,11 @@ public class ServerInfoDto /// /// Version of .NET instance is running /// - public string DotnetVersion { get; set; } + public required string DotnetVersion { get; set; } /// /// Version of Kavita /// - public string KavitaVersion { get; set; } + public required string KavitaVersion { get; set; } /// /// Number of Cores on the instance /// @@ -42,7 +41,7 @@ public class ServerInfoDto /// The site theme the install is using /// /// Introduced in v0.5.2 - public string ActiveSiteTheme { get; set; } + public string? ActiveSiteTheme { get; set; } /// /// The reading mode the main user has as a preference /// @@ -124,22 +123,22 @@ public class ServerInfoDto /// A list of background colors set on the instance /// /// Introduced in v0.6.0 - public IEnumerable MangaReaderBackgroundColors { get; set; } + public required IEnumerable MangaReaderBackgroundColors { get; set; } /// /// A list of Page Split defaults being used on the instance /// /// Introduced in v0.6.0 - public IEnumerable MangaReaderPageSplittingModes { get; set; } + public required IEnumerable MangaReaderPageSplittingModes { get; set; } /// /// A list of Layout Mode defaults being used on the instance /// /// Introduced in v0.6.0 - public IEnumerable MangaReaderLayoutModes { get; set; } + public required IEnumerable MangaReaderLayoutModes { get; set; } /// /// A list of file formats existing in the instance /// /// Introduced in v0.6.0 - public IEnumerable FileFormats { get; set; } + public required IEnumerable FileFormats { get; set; } /// /// If there is at least one user that is using an age restricted profile on the instance /// diff --git a/API/DTOs/System/DirectoryDto.cs b/API/DTOs/System/DirectoryDto.cs index 7f254c649..e6e94f4e4 100644 --- a/API/DTOs/System/DirectoryDto.cs +++ b/API/DTOs/System/DirectoryDto.cs @@ -5,9 +5,9 @@ public class DirectoryDto /// /// Name of the directory /// - public string Name { get; set; } + public string Name { get; set; } = default!; /// /// Full Directory Path /// - public string FullPath { get; set; } + public string FullPath { get; set; } = default!; } diff --git a/API/DTOs/Theme/SiteThemeDto.cs b/API/DTOs/Theme/SiteThemeDto.cs index 6e3650e21..b503c0100 100644 --- a/API/DTOs/Theme/SiteThemeDto.cs +++ b/API/DTOs/Theme/SiteThemeDto.cs @@ -14,12 +14,12 @@ public class SiteThemeDto : IEntityDate /// /// Name of the Theme /// - public string Name { get; set; } + public required string Name { get; set; } /// /// File path to the content. Stored under . /// Must be a .css file /// - public string FileName { get; set; } + public required string FileName { get; set; } /// /// Only one theme can have this. Will auto-set this as default for new user accounts /// diff --git a/API/DTOs/Update/UpdateNotificationDto.cs b/API/DTOs/Update/UpdateNotificationDto.cs index 030227a45..95719bb27 100644 --- a/API/DTOs/Update/UpdateNotificationDto.cs +++ b/API/DTOs/Update/UpdateNotificationDto.cs @@ -8,24 +8,24 @@ public class UpdateNotificationDto /// /// Current installed Version /// - public string CurrentVersion { get; init; } + public required string CurrentVersion { get; init; } /// /// Semver of the release version /// 0.4.3 /// - public string UpdateVersion { get; init; } + public required string UpdateVersion { get; init; } /// /// Release body in HTML /// - public string UpdateBody { get; init; } + public required string UpdateBody { get; init; } /// /// Title of the release /// - public string UpdateTitle { get; init; } + public required string UpdateTitle { get; init; } /// /// Github Url /// - public string UpdateUrl { get; init; } + public required string UpdateUrl { get; init; } /// /// If this install is within Docker /// @@ -37,5 +37,5 @@ public class UpdateNotificationDto /// /// Date of the publish /// - public string PublishDate { get; init; } + public required string PublishDate { get; init; } } diff --git a/API/DTOs/UpdateLibraryDto.cs b/API/DTOs/UpdateLibraryDto.cs index 206e5daef..db25be886 100644 --- a/API/DTOs/UpdateLibraryDto.cs +++ b/API/DTOs/UpdateLibraryDto.cs @@ -9,11 +9,11 @@ public class UpdateLibraryDto [Required] public int Id { get; init; } [Required] - public string Name { get; init; } + public required string Name { get; init; } [Required] public LibraryType Type { get; set; } [Required] - public IEnumerable Folders { get; init; } + public required IEnumerable Folders { get; init; } [Required] public bool FolderWatching { get; init; } [Required] diff --git a/API/DTOs/UpdateLibraryForUserDto.cs b/API/DTOs/UpdateLibraryForUserDto.cs index b2c752b22..c90b697e2 100644 --- a/API/DTOs/UpdateLibraryForUserDto.cs +++ b/API/DTOs/UpdateLibraryForUserDto.cs @@ -4,6 +4,6 @@ namespace API.DTOs; public class UpdateLibraryForUserDto { - public string Username { get; init; } - public IEnumerable SelectedLibraries { get; init; } + public required string Username { get; init; } + public required IEnumerable SelectedLibraries { get; init; } = new List(); } diff --git a/API/DTOs/UpdateRBSDto.cs b/API/DTOs/UpdateRBSDto.cs index f23edf784..6fdce251c 100644 --- a/API/DTOs/UpdateRBSDto.cs +++ b/API/DTOs/UpdateRBSDto.cs @@ -4,6 +4,6 @@ namespace API.DTOs; public class UpdateRbsDto { - public string Username { get; init; } - public IList Roles { get; init; } + public required string Username { get; init; } + public IList? Roles { get; init; } } diff --git a/API/DTOs/UpdateSeriesDto.cs b/API/DTOs/UpdateSeriesDto.cs index c5db42e78..a31152965 100644 --- a/API/DTOs/UpdateSeriesDto.cs +++ b/API/DTOs/UpdateSeriesDto.cs @@ -3,9 +3,9 @@ public class UpdateSeriesDto { public int Id { get; init; } - public string Name { get; init; } - public string LocalizedName { get; init; } - public string SortName { get; init; } + public required string Name { get; init; } + public string? LocalizedName { get; init; } + public string? SortName { get; init; } public bool CoverImageLocked { get; set; } public bool NameLocked { get; set; } diff --git a/API/DTOs/UpdateSeriesMetadataDto.cs b/API/DTOs/UpdateSeriesMetadataDto.cs index f2724b628..cdd6c7502 100644 --- a/API/DTOs/UpdateSeriesMetadataDto.cs +++ b/API/DTOs/UpdateSeriesMetadataDto.cs @@ -5,6 +5,6 @@ namespace API.DTOs; public class UpdateSeriesMetadataDto { - public SeriesMetadataDto SeriesMetadata { get; set; } - public ICollection CollectionTags { get; set; } + public SeriesMetadataDto SeriesMetadata { get; set; } = default!; + public ICollection CollectionTags { get; set; } = default!; } diff --git a/API/DTOs/UpdateSeriesRatingDto.cs b/API/DTOs/UpdateSeriesRatingDto.cs index 167d321bb..0b50aac78 100644 --- a/API/DTOs/UpdateSeriesRatingDto.cs +++ b/API/DTOs/UpdateSeriesRatingDto.cs @@ -7,5 +7,5 @@ public class UpdateSeriesRatingDto public int SeriesId { get; init; } public int UserRating { get; init; } [MaxLength(1000)] - public string UserReview { get; init; } + public string? UserReview { get; init; } } diff --git a/API/DTOs/Uploads/UploadFileDto.cs b/API/DTOs/Uploads/UploadFileDto.cs index 374f43b23..236a554b8 100644 --- a/API/DTOs/Uploads/UploadFileDto.cs +++ b/API/DTOs/Uploads/UploadFileDto.cs @@ -5,9 +5,9 @@ public class UploadFileDto /// /// Id of the Entity /// - public int Id { get; set; } + public required int Id { get; set; } /// /// Base Url encoding of the file to upload from (can be null) /// - public string Url { get; set; } + public required string Url { get; set; } } diff --git a/API/DTOs/Uploads/UploadUrlDto.cs b/API/DTOs/Uploads/UploadUrlDto.cs index cd44b78a2..f2699befd 100644 --- a/API/DTOs/Uploads/UploadUrlDto.cs +++ b/API/DTOs/Uploads/UploadUrlDto.cs @@ -1,9 +1,12 @@ -namespace API.DTOs.Uploads; +using System.ComponentModel.DataAnnotations; + +namespace API.DTOs.Uploads; public class UploadUrlDto { /// /// External url /// - public string Url { get; set; } + [Required] + public required string Url { get; set; } } diff --git a/API/DTOs/UserDto.cs b/API/DTOs/UserDto.cs index 1e9cba267..aad61b83e 100644 --- a/API/DTOs/UserDto.cs +++ b/API/DTOs/UserDto.cs @@ -1,16 +1,15 @@  using API.DTOs.Account; -using API.Entities.Enums; namespace API.DTOs; public class UserDto { - public string Username { get; init; } - public string Email { get; init; } - public string Token { get; set; } - public string RefreshToken { get; set; } - public string ApiKey { get; init; } - public UserPreferencesDto Preferences { get; set; } - public AgeRestrictionDto AgeRestriction { get; init; } + public string Username { get; init; } = null!; + public string Email { get; init; } = null!; + public string Token { get; set; } = null!; + public string? RefreshToken { get; set; } + public string? ApiKey { get; init; } + public UserPreferencesDto? Preferences { get; set; } + public AgeRestrictionDto? AgeRestriction { get; init; } } diff --git a/API/DTOs/UserPreferencesDto.cs b/API/DTOs/UserPreferencesDto.cs index b756534cb..41dbccf60 100644 --- a/API/DTOs/UserPreferencesDto.cs +++ b/API/DTOs/UserPreferencesDto.cs @@ -80,7 +80,8 @@ public class UserPreferencesDto /// Book Reader Option: Maps to the default Kavita font-family (inherit) or an override /// [Required] - public string BookReaderFontFamily { get; set; } + public string BookReaderFontFamily { get; set; } = null!; + /// /// Book Reader Option: Allows tapping on side of screens to paginate /// @@ -96,9 +97,10 @@ public class UserPreferencesDto /// UI Site Global Setting: The UI theme the user should use. /// /// Should default to Dark - public SiteTheme Theme { get; set; } [Required] - public string BookReaderThemeName { get; set; } + public SiteTheme? Theme { get; set; } + + [Required] public string BookReaderThemeName { get; set; } = null!; [Required] public BookPageLayoutMode BookReaderLayoutMode { get; set; } /// diff --git a/API/DTOs/VolumeDto.cs b/API/DTOs/VolumeDto.cs index 4ef20950a..8d5e615ee 100644 --- a/API/DTOs/VolumeDto.cs +++ b/API/DTOs/VolumeDto.cs @@ -11,14 +11,15 @@ public class VolumeDto : IHasReadTimeEstimate public int Id { get; set; } /// public int Number { get; set; } + /// - public string Name { get; set; } + public string Name { get; set; } = default!; public int Pages { get; set; } public int PagesRead { get; set; } public DateTime LastModified { get; set; } public DateTime Created { get; set; } public int SeriesId { get; set; } - public ICollection Chapters { get; set; } + public ICollection Chapters { get; set; } = new List(); /// public int MinHoursToRead { get; set; } /// diff --git a/API/DTOs/WantToRead/UpdateWantToReadDto.cs b/API/DTOs/WantToRead/UpdateWantToReadDto.cs index 14a1a4710..f1b38cea2 100644 --- a/API/DTOs/WantToRead/UpdateWantToReadDto.cs +++ b/API/DTOs/WantToRead/UpdateWantToReadDto.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace API.DTOs.WantToRead; @@ -10,5 +11,5 @@ public class UpdateWantToReadDto /// /// List of Series Ids that will be Added/Removed /// - public IList SeriesIds { get; set; } + public IList SeriesIds { get; set; } = ArraySegment.Empty; } diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index 9a6af8b3e..e44da6913 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -23,29 +23,29 @@ public sealed class DataContext : IdentityDbContext Library { get; set; } - public DbSet Series { get; set; } - public DbSet Chapter { get; set; } - public DbSet Volume { get; set; } - public DbSet AppUser { get; set; } - public DbSet MangaFile { get; set; } - public DbSet AppUserProgresses { get; set; } - public DbSet AppUserRating { get; set; } - public DbSet ServerSetting { get; set; } - public DbSet AppUserPreferences { get; set; } - public DbSet SeriesMetadata { get; set; } - public DbSet CollectionTag { get; set; } - public DbSet AppUserBookmark { get; set; } - public DbSet ReadingList { get; set; } - public DbSet ReadingListItem { get; set; } - public DbSet Person { get; set; } - public DbSet Genre { get; set; } - public DbSet Tag { get; set; } - public DbSet SiteTheme { get; set; } - public DbSet SeriesRelation { get; set; } - public DbSet FolderPath { get; set; } - public DbSet Device { get; set; } - public DbSet ServerStatistics { get; set; } + public DbSet Library { get; set; } = null!; + public DbSet Series { get; set; } = null!; + public DbSet Chapter { get; set; } = null!; + public DbSet Volume { get; set; } = null!; + public DbSet AppUser { get; set; } = null!; + public DbSet MangaFile { get; set; } = null!; + public DbSet AppUserProgresses { get; set; } = null!; + public DbSet AppUserRating { get; set; } = null!; + public DbSet ServerSetting { get; set; } = null!; + public DbSet AppUserPreferences { get; set; } = null!; + public DbSet SeriesMetadata { get; set; } = null!; + public DbSet CollectionTag { get; set; } = null!; + public DbSet AppUserBookmark { get; set; } = null!; + public DbSet ReadingList { get; set; } = null!; + public DbSet ReadingListItem { get; set; } = null!; + public DbSet Person { get; set; } = null!; + public DbSet Genre { get; set; } = null!; + public DbSet Tag { get; set; } = null!; + public DbSet SiteTheme { get; set; } = null!; + public DbSet SeriesRelation { get; set; } = null!; + public DbSet FolderPath { get; set; } = null!; + public DbSet Device { get; set; } = null!; + public DbSet ServerStatistics { get; set; } = null!; protected override void OnModelCreating(ModelBuilder builder) @@ -110,7 +110,7 @@ public sealed class DataContext : IdentityDbContext SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken)) { - this.OnSaveChanges(); + OnSaveChanges(); return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); } public override Task SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken)) { - this.OnSaveChanges(); + OnSaveChanges(); return base.SaveChangesAsync(cancellationToken); } diff --git a/API/Data/DbFactory.cs b/API/Data/DbFactory.cs index 11304d05c..ec58af094 100644 --- a/API/Data/DbFactory.cs +++ b/API/Data/DbFactory.cs @@ -1,13 +1,12 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; -using API.Data.Metadata; using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; using API.Extensions; using API.Parser; using API.Services.Tasks; +using Kavita.Common; namespace API.Data; @@ -16,6 +15,17 @@ namespace API.Data; /// public static class DbFactory { + public static Library Library(string name, LibraryType type) + { + return new Library() + { + Name = name, + Type = type, + Series = new List(), + Folders = new List(), + AppUsers = new List() + }; + } public static Series Series(string name) { return new Series @@ -23,8 +33,8 @@ public static class DbFactory Name = name, OriginalName = name, LocalizedName = name, - NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name), - NormalizedLocalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name), + NormalizedName = name.ToNormalized(), + NormalizedLocalizedName = name.ToNormalized(), SortName = name, Volumes = new List(), Metadata = SeriesMetadata(new List()) @@ -42,8 +52,8 @@ public static class DbFactory Name = name, OriginalName = name, LocalizedName = localizedName, - NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name), - NormalizedLocalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(localizedName), + NormalizedName = name.ToNormalized(), + NormalizedLocalizedName = localizedName.ToNormalized(), SortName = name, Volumes = new List(), Metadata = SeriesMetadata(new List()) @@ -85,28 +95,31 @@ public static class DbFactory }; } - public static CollectionTag CollectionTag(int id, string title, string summary, bool promoted) + public static CollectionTag CollectionTag(int id, string title, string? summary = null, bool promoted = false) { + title = title.Trim(); return new CollectionTag() { Id = id, - NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(title?.Trim()), - Title = title?.Trim(), + NormalizedTitle = title.ToNormalized(), + Title = title, Summary = summary?.Trim(), Promoted = promoted, SeriesMetadatas = new List() }; } - public static ReadingList ReadingList(string title, string summary, bool promoted) + public static ReadingList ReadingList(string title, string? summary = null, bool promoted = false, AgeRating rating = AgeRating.Unknown) { + title = title.Trim(); return new ReadingList() { - NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(title?.Trim()), - Title = title?.Trim(), + NormalizedTitle = title.ToNormalized(), + Title = title, Summary = summary?.Trim(), Promoted = promoted, - Items = new List() + Items = new List(), + AgeRating = rating }; } @@ -126,7 +139,7 @@ public static class DbFactory return new Genre() { Title = name.Trim().SentenceCase(), - NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(name), + NormalizedTitle = name.ToNormalized() }; } @@ -135,7 +148,7 @@ public static class DbFactory return new Tag() { Title = name.Trim().SentenceCase(), - NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(name), + NormalizedTitle = name.ToNormalized() }; } @@ -144,7 +157,7 @@ public static class DbFactory return new Person() { Name = name.Trim(), - NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name), + NormalizedName = name.ToNormalized(), Role = role }; } @@ -169,4 +182,17 @@ public static class DbFactory }; } + public static AppUser AppUser(string username, string email, SiteTheme defaultTheme) + { + return new AppUser() + { + UserName = username, + Email = email, + ApiKey = HashUtil.ApiKey(), + UserPreferences = new AppUserPreferences + { + Theme = defaultTheme + } + }; + } } diff --git a/API/Data/Metadata/ComicInfo.cs b/API/Data/Metadata/ComicInfo.cs index 1907878cb..1a4654815 100644 --- a/API/Data/Metadata/ComicInfo.cs +++ b/API/Data/Metadata/ComicInfo.cs @@ -119,7 +119,7 @@ public class ComicInfo .SingleOrDefault(t => t.ToDescription().ToUpperInvariant().Equals(value.ToUpperInvariant()), Entities.Enums.AgeRating.Unknown); } - public static void CleanComicInfo(ComicInfo info) + public static void CleanComicInfo(ComicInfo? info) { if (info == null) return; diff --git a/API/Data/MigrateNormalizedEverything.cs b/API/Data/MigrateNormalizedEverything.cs index 675620225..69a3e2728 100644 --- a/API/Data/MigrateNormalizedEverything.cs +++ b/API/Data/MigrateNormalizedEverything.cs @@ -1,7 +1,5 @@ using System; -using System.Linq; using System.Threading.Tasks; -using Kavita.Common.EnvironmentInfo; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; diff --git a/API/Data/MigrateReadingListAgeRating.cs b/API/Data/MigrateReadingListAgeRating.cs index cc1ddfc3d..b057d702b 100644 --- a/API/Data/MigrateReadingListAgeRating.cs +++ b/API/Data/MigrateReadingListAgeRating.cs @@ -1,10 +1,8 @@ using System; using System.Threading.Tasks; -using API.Constants; using API.Services; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using SQLitePCL; namespace API.Data; diff --git a/API/Data/MigrateSeriesRelationsExport.cs b/API/Data/MigrateSeriesRelationsExport.cs index 874769708..f31688641 100644 --- a/API/Data/MigrateSeriesRelationsExport.cs +++ b/API/Data/MigrateSeriesRelationsExport.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using System.Text; using System.Threading.Tasks; using API.Entities.Enums; using CsvHelper; @@ -15,9 +14,9 @@ namespace API.Data; internal sealed class SeriesRelationMigrationOutput { - public string SeriesName { get; set; } + public required string SeriesName { get; set; } public int SeriesId { get; set; } - public string TargetSeriesName { get; set; } + public required string TargetSeriesName { get; set; } public int TargetId { get; set; } public RelationKind Relationship { get; set; } } diff --git a/API/Data/MigrateSeriesRelationsImport.cs b/API/Data/MigrateSeriesRelationsImport.cs index dc4938d8a..8035e8c4b 100644 --- a/API/Data/MigrateSeriesRelationsImport.cs +++ b/API/Data/MigrateSeriesRelationsImport.cs @@ -2,9 +2,7 @@ using System.Globalization; using System.IO; using System.Linq; -using System.Text; using System.Threading.Tasks; -using API.Entities.Enums; using API.Entities.Metadata; using CsvHelper; using Microsoft.EntityFrameworkCore; diff --git a/API/Data/MigrateUserProgressLibraryId.cs b/API/Data/MigrateUserProgressLibraryId.cs index 8b4d84f3f..78e9933da 100644 --- a/API/Data/MigrateUserProgressLibraryId.cs +++ b/API/Data/MigrateUserProgressLibraryId.cs @@ -1,13 +1,4 @@ -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using API.Entities.Enums; -using API.Entities.Metadata; -using CsvHelper; -using Microsoft.EntityFrameworkCore; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace API.Data; diff --git a/API/Data/Misc/RecentlyAddedSeries.cs b/API/Data/Misc/RecentlyAddedSeries.cs index 24100ca0f..684247d9c 100644 --- a/API/Data/Misc/RecentlyAddedSeries.cs +++ b/API/Data/Misc/RecentlyAddedSeries.cs @@ -9,13 +9,13 @@ public class RecentlyAddedSeries public LibraryType LibraryType { get; init; } public DateTime Created { get; init; } public int SeriesId { get; init; } - public string SeriesName { get; init; } + public string? SeriesName { get; init; } public MangaFormat Format { get; init; } public int ChapterId { get; init; } public int VolumeId { get; init; } - public string ChapterNumber { get; init; } - public string ChapterRange { get; init; } - public string ChapterTitle { get; init; } + public string? ChapterNumber { get; init; } + public string? ChapterRange { get; init; } + public string? ChapterTitle { get; init; } public bool IsSpecial { get; init; } public int VolumeNumber { get; init; } public AgeRating AgeRating { get; init; } diff --git a/API/Data/Repositories/AppUserProgressRepository.cs b/API/Data/Repositories/AppUserProgressRepository.cs index af442081e..4a1a9d75c 100644 --- a/API/Data/Repositories/AppUserProgressRepository.cs +++ b/API/Data/Repositories/AppUserProgressRepository.cs @@ -15,13 +15,13 @@ public interface IAppUserProgressRepository void Update(AppUserProgress userProgress); Task CleanupAbandonedChapters(); Task UserHasProgress(LibraryType libraryType, int userId); - Task GetUserProgressAsync(int chapterId, int userId); + Task GetUserProgressAsync(int chapterId, int userId); Task HasAnyProgressOnSeriesAsync(int seriesId, int userId); /// /// This is built exclusively for /// /// - Task GetAnyProgress(); + Task GetAnyProgress(); Task> GetUserProgressForSeriesAsync(int seriesId, int userId); Task> GetAllProgress(); Task GetUserProgressDtoAsync(int chapterId, int userId); @@ -97,7 +97,7 @@ public class AppUserProgressRepository : IAppUserProgressRepository .AnyAsync(aup => aup.PagesRead > 0 && aup.AppUserId == userId && aup.SeriesId == seriesId); } - public async Task GetAnyProgress() + public async Task GetAnyProgress() { return await _context.AppUserProgresses.FirstOrDefaultAsync(); } @@ -119,7 +119,7 @@ public class AppUserProgressRepository : IAppUserProgressRepository { return await _context.AppUserProgresses.ToListAsync(); } - + public async Task GetUserProgressDtoAsync(int chapterId, int userId) { return await _context.AppUserProgresses @@ -128,7 +128,7 @@ public class AppUserProgressRepository : IAppUserProgressRepository .FirstOrDefaultAsync(); } - public async Task GetUserProgressAsync(int chapterId, int userId) + public async Task GetUserProgressAsync(int chapterId, int userId) { return await _context.AppUserProgresses .Where(p => p.ChapterId == chapterId && p.AppUserId == userId) diff --git a/API/Data/Repositories/ChapterRepository.cs b/API/Data/Repositories/ChapterRepository.cs index 82944fe77..81905f04d 100644 --- a/API/Data/Repositories/ChapterRepository.cs +++ b/API/Data/Repositories/ChapterRepository.cs @@ -25,15 +25,15 @@ public interface IChapterRepository { void Update(Chapter chapter); Task> GetChaptersByIdsAsync(IList chapterIds, ChapterIncludes includes = ChapterIncludes.None); - Task GetChapterInfoDtoAsync(int chapterId); + Task GetChapterInfoDtoAsync(int chapterId); Task GetChapterTotalPagesAsync(int chapterId); - Task GetChapterAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files); - Task GetChapterDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files); - Task GetChapterMetadataDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files); + Task GetChapterAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files); + Task GetChapterDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files); + Task GetChapterMetadataDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files); Task> GetFilesForChapterAsync(int chapterId); Task> GetChaptersAsync(int volumeId); Task> GetFilesForChaptersAsync(IReadOnlyList chapterIds); - Task GetChapterCoverImageAsync(int chapterId); + Task GetChapterCoverImageAsync(int chapterId); Task> GetAllCoverImagesAsync(); Task> GetAllChaptersWithNonWebPCovers(); Task> GetCoverImagesForLockedChaptersAsync(); @@ -68,7 +68,7 @@ public class ChapterRepository : IChapterRepository /// Populates a partial IChapterInfoDto /// /// - public async Task GetChapterInfoDtoAsync(int chapterId) + public async Task GetChapterInfoDtoAsync(int chapterId) { var chapterInfo = await _context.Chapter .Where(c => c.Id == chapterId) @@ -124,7 +124,7 @@ public class ChapterRepository : IChapterRepository .Select(c => c.Pages) .FirstOrDefaultAsync(); } - public async Task GetChapterDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files) + public async Task GetChapterDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files) { var chapter = await _context.Chapter .Includes(includes) @@ -136,7 +136,7 @@ public class ChapterRepository : IChapterRepository return chapter; } - public async Task GetChapterMetadataDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files) + public async Task GetChapterMetadataDtoAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files) { var chapter = await _context.Chapter .Includes(includes) @@ -167,7 +167,7 @@ public class ChapterRepository : IChapterRepository /// /// /// - public async Task GetChapterAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files) + public async Task GetChapterAsync(int chapterId, ChapterIncludes includes = ChapterIncludes.Files) { return await _context.Chapter .Includes(includes) @@ -191,23 +191,20 @@ public class ChapterRepository : IChapterRepository /// /// /// - public async Task GetChapterCoverImageAsync(int chapterId) + public async Task GetChapterCoverImageAsync(int chapterId) { - return await _context.Chapter .Where(c => c.Id == chapterId) .Select(c => c.CoverImage) - .AsNoTracking() .SingleOrDefaultAsync(); } public async Task> GetAllCoverImagesAsync() { - return await _context.Chapter + return (await _context.Chapter .Select(c => c.CoverImage) .Where(t => !string.IsNullOrEmpty(t)) - .AsNoTracking() - .ToListAsync(); + .ToListAsync())!; } public async Task> GetAllChaptersWithNonWebPCovers() @@ -223,12 +220,11 @@ public class ChapterRepository : IChapterRepository /// public async Task> GetCoverImagesForLockedChaptersAsync() { - return await _context.Chapter + return (await _context.Chapter .Where(c => c.CoverImageLocked) .Select(c => c.CoverImage) .Where(t => !string.IsNullOrEmpty(t)) - .AsNoTracking() - .ToListAsync(); + .ToListAsync())!; } /// diff --git a/API/Data/Repositories/CollectionTagRepository.cs b/API/Data/Repositories/CollectionTagRepository.cs index 5038a5892..0de0216e7 100644 --- a/API/Data/Repositories/CollectionTagRepository.cs +++ b/API/Data/Repositories/CollectionTagRepository.cs @@ -25,10 +25,9 @@ public interface ICollectionTagRepository void Remove(CollectionTag tag); Task> GetAllTagDtosAsync(); Task> SearchTagDtosAsync(string searchQuery, int userId); - Task GetCoverImageAsync(int collectionTagId); + Task GetCoverImageAsync(int collectionTagId); Task> GetAllPromotedTagDtosAsync(int userId); - Task GetTagAsync(int tagId); - Task GetFullTagAsync(int tagId, CollectionTagIncludes includes = CollectionTagIncludes.SeriesMetadata); + Task GetTagAsync(int tagId, CollectionTagIncludes includes = CollectionTagIncludes.None); void Update(CollectionTag tag); Task RemoveTagsWithoutSeries(); Task> GetAllTagsAsync(CollectionTagIncludes includes = CollectionTagIncludes.None); @@ -84,29 +83,27 @@ public class CollectionTagRepository : ICollectionTagRepository .ToListAsync(); } - public async Task GetCoverImageAsync(int collectionTagId) + public async Task GetCoverImageAsync(int collectionTagId) { return await _context.CollectionTag .Where(c => c.Id == collectionTagId) .Select(c => c.CoverImage) - .AsNoTracking() .SingleOrDefaultAsync(); } public async Task> GetAllCoverImagesAsync() { - return await _context.CollectionTag + return (await _context.CollectionTag .Select(t => t.CoverImage) .Where(t => !string.IsNullOrEmpty(t)) - .AsNoTracking() - .ToListAsync(); + .ToListAsync())!; } public async Task TagExists(string title) { - var normalized = Services.Tasks.Scanner.Parser.Parser.Normalize(title); + var normalized = title.ToNormalized(); return await _context.CollectionTag - .AnyAsync(x => x.NormalizedTitle.Equals(normalized)); + .AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized)); } public async Task> GetAllTagDtosAsync() @@ -131,14 +128,8 @@ public class CollectionTagRepository : ICollectionTagRepository .ToListAsync(); } - public async Task GetTagAsync(int tagId) - { - return await _context.CollectionTag - .Where(c => c.Id == tagId) - .SingleOrDefaultAsync(); - } - public async Task GetFullTagAsync(int tagId, CollectionTagIncludes includes = CollectionTagIncludes.SeriesMetadata) + public async Task GetTagAsync(int tagId, CollectionTagIncludes includes = CollectionTagIncludes.None) { return await _context.CollectionTag .Where(c => c.Id == tagId) @@ -164,8 +155,8 @@ public class CollectionTagRepository : ICollectionTagRepository { var userRating = await GetUserAgeRestriction(userId); return await _context.CollectionTag - .Where(s => EF.Functions.Like(s.Title, $"%{searchQuery}%") - || EF.Functions.Like(s.NormalizedTitle, $"%{searchQuery}%")) + .Where(s => EF.Functions.Like(s.Title!, $"%{searchQuery}%") + || EF.Functions.Like(s.NormalizedTitle!, $"%{searchQuery}%")) .RestrictAgainstAgeRestriction(userRating) .OrderBy(s => s.NormalizedTitle) .AsNoTracking() diff --git a/API/Data/Repositories/DeviceRepository.cs b/API/Data/Repositories/DeviceRepository.cs index b6f139bc1..479f5604b 100644 --- a/API/Data/Repositories/DeviceRepository.cs +++ b/API/Data/Repositories/DeviceRepository.cs @@ -13,7 +13,7 @@ public interface IDeviceRepository { void Update(Device device); Task> GetDevicesForUserAsync(int userId); - Task GetDeviceById(int deviceId); + Task GetDeviceById(int deviceId); } public class DeviceRepository : IDeviceRepository @@ -41,7 +41,7 @@ public class DeviceRepository : IDeviceRepository .ToListAsync(); } - public async Task GetDeviceById(int deviceId) + public async Task GetDeviceById(int deviceId) { return await _context.Device .Where(d => d.Id == deviceId) diff --git a/API/Data/Repositories/GenreRepository.cs b/API/Data/Repositories/GenreRepository.cs index 2533a40cf..14e575ef6 100644 --- a/API/Data/Repositories/GenreRepository.cs +++ b/API/Data/Repositories/GenreRepository.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using API.Data.Misc; using API.DTOs.Metadata; using API.Entities; using API.Extensions; @@ -15,7 +14,7 @@ public interface IGenreRepository { void Attach(Genre genre); void Remove(Genre genre); - Task FindByNameAsync(string genreName); + Task FindByNameAsync(string genreName); Task> GetAllGenresAsync(); Task> GetAllGenreDtosAsync(int userId); Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false); @@ -44,11 +43,11 @@ public class GenreRepository : IGenreRepository _context.Genre.Remove(genre); } - public async Task FindByNameAsync(string genreName) + public async Task FindByNameAsync(string genreName) { - var normalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(genreName); + var normalizedName = genreName.ToNormalized(); return await _context.Genre - .FirstOrDefaultAsync(g => g.NormalizedTitle.Equals(normalizedName)); + .FirstOrDefaultAsync(g => g.NormalizedTitle != null && g.NormalizedTitle.Equals(normalizedName)); } public async Task RemoveAllGenreNoLongerAssociated(bool removeExternal = false) diff --git a/API/Data/Repositories/LibraryRepository.cs b/API/Data/Repositories/LibraryRepository.cs index 1b2303e98..f8cc72449 100644 --- a/API/Data/Repositories/LibraryRepository.cs +++ b/API/Data/Repositories/LibraryRepository.cs @@ -31,13 +31,12 @@ public interface ILibraryRepository { void Add(Library library); void Update(Library library); - void Delete(Library library); + void Delete(Library? library); Task> GetLibraryDtosAsync(); Task LibraryExists(string libraryName); - Task GetLibraryForIdAsync(int libraryId, LibraryIncludes includes = LibraryIncludes.None); + Task GetLibraryForIdAsync(int libraryId, LibraryIncludes includes = LibraryIncludes.None); Task> GetLibraryDtosForUsernameAsync(string userName); Task> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None); - Task DeleteLibrary(int libraryId); Task> GetLibrariesForUserIdAsync(int userId); IEnumerable GetLibraryIdsForUserIdAsync(int userId, QueryContext queryContext = QueryContext.None); Task GetLibraryTypeAsync(int libraryId); @@ -49,7 +48,7 @@ public interface ILibraryRepository Task> GetAllLanguagesForLibrariesAsync(); IEnumerable GetAllPublicationStatusesDtosForLibrariesAsync(List libraryIds); Task DoAnySeriesFoldersMatch(IEnumerable folders); - Task GetLibraryCoverImageAsync(int libraryId); + Task GetLibraryCoverImageAsync(int libraryId); Task> GetAllCoverImagesAsync(); Task> GetLibraryTypesForIdsAsync(IEnumerable libraryIds); } @@ -75,8 +74,9 @@ public class LibraryRepository : ILibraryRepository _context.Entry(library).State = EntityState.Modified; } - public void Delete(Library library) + public void Delete(Library? library) { + if (library == null) return; _context.Library.Remove(library); } @@ -107,14 +107,6 @@ public class LibraryRepository : ILibraryRepository return await query.ToListAsync(); } - public async Task DeleteLibrary(int libraryId) - { - var library = await GetLibraryForIdAsync(libraryId, LibraryIncludes.Folders | LibraryIncludes.Series); - _context.Library.Remove(library); - - return await _context.SaveChangesAsync() > 0; - } - /// /// This does not track /// @@ -164,7 +156,7 @@ public class LibraryRepository : ILibraryRepository public IEnumerable GetJumpBarAsync(int libraryId) { var seriesSortCharacters = _context.Series.Where(s => s.LibraryId == libraryId) - .Select(s => s.SortName.ToUpper()) + .Select(s => s.SortName!.ToUpper()) .OrderBy(s => s) .AsEnumerable() .Select(s => s[0]); @@ -207,7 +199,7 @@ public class LibraryRepository : ILibraryRepository .ToListAsync(); } - public async Task GetLibraryForIdAsync(int libraryId, LibraryIncludes includes = LibraryIncludes.None) + public async Task GetLibraryForIdAsync(int libraryId, LibraryIncludes includes = LibraryIncludes.None) { var query = _context.Library @@ -237,54 +229,11 @@ public class LibraryRepository : ILibraryRepository return query.AsSplitQuery(); } - - /// - /// This returns a Library with all it's Series -> Volumes -> Chapters. This is expensive. Should only be called when needed. - /// - /// - /// - public async Task GetFullLibraryForIdAsync(int libraryId) - { - return await _context.Library - .Where(x => x.Id == libraryId) - .Include(f => f.Folders) - .Include(l => l.Series) - .ThenInclude(s => s.Metadata) - .Include(l => l.Series) - .ThenInclude(s => s.Volumes) - .ThenInclude(v => v.Chapters) - .ThenInclude(c => c.Files) - .AsSplitQuery() - .SingleAsync(); - } - - /// - /// This is a heavy call, pulls all entities for a Library, except this version only grabs for one series id - /// - /// - /// - /// - public async Task GetFullLibraryForIdAsync(int libraryId, int seriesId) - { - - return await _context.Library - .Where(x => x.Id == libraryId) - .Include(f => f.Folders) - .Include(l => l.Series.Where(s => s.Id == seriesId)) - .ThenInclude(s => s.Metadata) - .Include(l => l.Series.Where(s => s.Id == seriesId)) - .ThenInclude(s => s.Volumes) - .ThenInclude(v => v.Chapters) - .ThenInclude(c => c.Files) - .AsSplitQuery() - .SingleAsync(); - } - public async Task LibraryExists(string libraryName) { return await _context.Library .AsNoTracking() - .AnyAsync(x => x.Name.Equals(libraryName)); + .AnyAsync(x => x.Name != null && x.Name.Equals(libraryName)); } public async Task> GetLibrariesForUserAsync(AppUser user) @@ -381,7 +330,7 @@ public class LibraryRepository : ILibraryRepository return await _context.Series.AnyAsync(s => normalized.Contains(s.FolderPath)); } - public Task GetLibraryCoverImageAsync(int libraryId) + public Task GetLibraryCoverImageAsync(int libraryId) { return _context.Library .Where(l => l.Id == libraryId) @@ -392,11 +341,10 @@ public class LibraryRepository : ILibraryRepository public async Task> GetAllCoverImagesAsync() { - return await _context.ReadingList + return (await _context.ReadingList .Select(t => t.CoverImage) .Where(t => !string.IsNullOrEmpty(t)) - .AsNoTracking() - .ToListAsync(); + .ToListAsync())!; } public async Task> GetLibraryTypesForIdsAsync(IEnumerable libraryIds) diff --git a/API/Data/Repositories/MangaFileRepository.cs b/API/Data/Repositories/MangaFileRepository.cs index e45700b32..ddea0d51a 100644 --- a/API/Data/Repositories/MangaFileRepository.cs +++ b/API/Data/Repositories/MangaFileRepository.cs @@ -2,7 +2,6 @@ using System.Linq; using System.Threading.Tasks; using API.Entities; -using AutoMapper; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; @@ -17,12 +16,10 @@ public interface IMangaFileRepository public class MangaFileRepository : IMangaFileRepository { private readonly DataContext _context; - private readonly IMapper _mapper; - public MangaFileRepository(DataContext context, IMapper mapper) + public MangaFileRepository(DataContext context) { _context = context; - _mapper = mapper; } public void Update(MangaFile file) diff --git a/API/Data/Repositories/ReadingListRepository.cs b/API/Data/Repositories/ReadingListRepository.cs index 1719f67b2..3064af422 100644 --- a/API/Data/Repositories/ReadingListRepository.cs +++ b/API/Data/Repositories/ReadingListRepository.cs @@ -1,10 +1,9 @@ -using System.Collections; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.DTOs.ReadingLists; using API.Entities; -using API.Entities.Enums; +using API.Extensions; using API.Helpers; using API.Services; using AutoMapper; @@ -16,11 +15,11 @@ namespace API.Data.Repositories; public interface IReadingListRepository { Task> GetReadingListDtosForUserAsync(int userId, bool includePromoted, UserParams userParams); - Task GetReadingListByIdAsync(int readingListId); + Task GetReadingListByIdAsync(int readingListId); Task> GetReadingListItemDtosByIdAsync(int readingListId, int userId); - Task GetReadingListDtoByIdAsync(int readingListId, int userId); + Task GetReadingListDtoByIdAsync(int readingListId, int userId); Task> AddReadingProgressModifiers(int userId, IList items); - Task GetReadingListDtoByTitleAsync(int userId, string title); + Task GetReadingListDtoByTitleAsync(int userId, string title); Task> GetReadingListItemsByIdAsync(int readingListId); Task> GetReadingListDtosForSeriesAndUserAsync(int userId, int seriesId, bool includePromoted); @@ -29,7 +28,7 @@ public interface IReadingListRepository void BulkRemove(IEnumerable items); void Update(ReadingList list); Task Count(); - Task GetCoverImageAsync(int readingListId); + Task GetCoverImageAsync(int readingListId); Task> GetAllCoverImagesAsync(); Task ReadingListExists(string name); Task> GetAllReadingListsAsync(); @@ -61,29 +60,27 @@ public class ReadingListRepository : IReadingListRepository return await _context.ReadingList.CountAsync(); } - public async Task GetCoverImageAsync(int readingListId) + public async Task GetCoverImageAsync(int readingListId) { return await _context.ReadingList .Where(c => c.Id == readingListId) .Select(c => c.CoverImage) - .AsNoTracking() .SingleOrDefaultAsync(); } public async Task> GetAllCoverImagesAsync() { - return await _context.ReadingList + return (await _context.ReadingList .Select(t => t.CoverImage) .Where(t => !string.IsNullOrEmpty(t)) - .AsNoTracking() - .ToListAsync(); + .ToListAsync())!; } public async Task ReadingListExists(string name) { - var normalized = Services.Tasks.Scanner.Parser.Parser.Normalize(name); + var normalized = name.ToNormalized(); return await _context.ReadingList - .AnyAsync(x => x.NormalizedTitle.Equals(normalized)); + .AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized)); } public async Task> GetAllReadingListsAsync() @@ -132,7 +129,7 @@ public class ReadingListRepository : IReadingListRepository return await query.ToListAsync(); } - public async Task GetReadingListByIdAsync(int readingListId) + public async Task GetReadingListByIdAsync(int readingListId) { return await _context.ReadingList .Where(r => r.Id == readingListId) @@ -241,7 +238,7 @@ public class ReadingListRepository : IReadingListRepository return items; } - public async Task GetReadingListDtoByIdAsync(int readingListId, int userId) + public async Task GetReadingListDtoByIdAsync(int readingListId, int userId) { return await _context.ReadingList .Where(r => r.Id == readingListId && (r.AppUserId == userId || r.Promoted)) @@ -261,14 +258,14 @@ public class ReadingListRepository : IReadingListRepository { var progress = userProgress.Where(p => p.ChapterId == item.ChapterId).ToList(); if (progress.Count == 0) continue; - item.PagesRead = progress.Sum(p => p.PagesRead); + item.PagesRead = progress.Sum(p => p.PagesRead); item.LastReadingProgressUtc = progress.Max(p => p.LastModifiedUtc); } return items; } - public async Task GetReadingListDtoByTitleAsync(int userId, string title) + public async Task GetReadingListDtoByTitleAsync(int userId, string title) { return await _context.ReadingList .Where(r => r.Title.Equals(title) && r.AppUserId == userId) diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index e485acd54..26e5177de 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -78,7 +78,7 @@ public interface ISeriesRepository Task SearchSeries(int userId, bool isAdmin, IList libraryIds, string searchQuery); 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 GetSeriesByIdAsync(int seriesId, SeriesIncludes includes = SeriesIncludes.Volumes | SeriesIncludes.Metadata); Task> GetSeriesByIdsAsync(IList seriesIds); Task GetChapterIdsForSeriesAsync(IList seriesIds); Task>> GetChapterIdWithSeriesIdForSeriesAsync(int[] seriesIds); @@ -89,20 +89,19 @@ public interface ISeriesRepository /// /// Task AddSeriesModifiers(int userId, List series); - Task GetSeriesCoverImageAsync(int seriesId); + 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 GetSeriesMetadata(int seriesId); + Task GetSeriesMetadata(int seriesId); Task> GetSeriesDtoForCollectionAsync(int collectionId, int userId, UserParams userParams); Task> GetFilesForSeries(int seriesId); Task> GetSeriesDtoForIdsAsync(IEnumerable seriesIds, int userId); Task> GetAllCoverImagesAsync(); Task> GetLockedCoverImagesAsync(); Task> GetFullSeriesForLibraryIdAsync(int libraryId, UserParams userParams); - Task GetFullSeriesForSeriesIdAsync(int seriesId); + Task GetFullSeriesForSeriesIdAsync(int seriesId); Task GetChunkInfo(int libraryId = 0); Task> GetSeriesMetadataForIdsAsync(IEnumerable seriesIds); - Task> GetRecentlyUpdatedSeries(int userId, int pageSize = 30); Task GetRelatedSeries(int userId, int seriesId); Task> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind); @@ -111,20 +110,19 @@ public interface ISeriesRepository 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 GetSeriesForMangaFile(int mangaFileId, int userId); + Task GetSeriesForChapter(int chapterId, int userId); Task> GetWantToReadForUserAsync(int userId, UserParams userParams, FilterDto filter); Task IsSeriesInWantToRead(int userId, int seriesId); - Task GetSeriesByFolderPath(string folder, SeriesIncludes includes = SeriesIncludes.None); - + Task GetSeriesByFolderPath(string folder, SeriesIncludes includes = SeriesIncludes.None); Task> GetAllSeriesByNameAsync(IList normalizedNames, int userId, SeriesIncludes includes = SeriesIncludes.None); Task> GetAllSeriesDtosByNameAsync(IEnumerable normalizedNames, int userId, SeriesIncludes includes = SeriesIncludes.None); - Task GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, MangaFormat format, bool withFullIncludes = true); + Task GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, MangaFormat format, bool withFullIncludes = true); Task> RemoveSeriesNotInList(IList seenSeries, int libraryId); Task>> GetFolderPathMap(int libraryId); - Task GetMaxAgeRatingFromSeriesAsync(IEnumerable seriesIds); + Task GetMaxAgeRatingFromSeriesAsync(IEnumerable seriesIds); /// /// This is only used for /// @@ -138,6 +136,12 @@ public class SeriesRepository : ISeriesRepository { private readonly DataContext _context; private readonly IMapper _mapper; + + + // [GeneratedRegex(@"\d{4}", RegexOptions.Compiled, 50000)] + // private static partial Regex YearRegex(); + private readonly Regex _yearRegex = new Regex(@"\d{4}", RegexOptions.Compiled, Services.Tasks.Scanner.Parser.Parser.RegexTimeout); + public SeriesRepository(DataContext context, IMapper mapper) { _context = context; @@ -202,6 +206,7 @@ public class SeriesRepository : ISeriesRepository /// public async Task> GetFullSeriesForLibraryIdAsync(int libraryId, UserParams userParams) { + #nullable disable var query = _context.Series .Where(s => s.LibraryId == libraryId) @@ -229,11 +234,12 @@ public class SeriesRepository : ISeriesRepository .ThenInclude(v => v.Chapters) .ThenInclude(c => c.Tags) - .Include(s => s.Volumes) - .ThenInclude(v => v.Chapters) + .Include(s => s.Volumes)! + .ThenInclude(v => v.Chapters)! .ThenInclude(c => c.Files) .AsSplitQuery() .OrderBy(s => s.SortName.ToLower()); +#nullable enable return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); } @@ -243,8 +249,9 @@ public class SeriesRepository : ISeriesRepository /// /// /// - public async Task GetFullSeriesForSeriesIdAsync(int seriesId) + public async Task GetFullSeriesForSeriesIdAsync(int seriesId) { + #nullable disable return await _context.Series .Where(s => s.Id == seriesId) .Include(s => s.Relations) @@ -274,6 +281,7 @@ public class SeriesRepository : ISeriesRepository .ThenInclude(c => c.Files) .AsSplitQuery() .SingleOrDefaultAsync(); + #nullable enable } /// @@ -313,7 +321,7 @@ public class SeriesRepository : ISeriesRepository { const int maxRecords = 15; var result = new SearchResultGroupDto(); - var searchQueryNormalized = Services.Tasks.Scanner.Parser.Parser.Normalize(searchQuery); + var searchQueryNormalized = searchQuery.ToNormalized(); var userRating = await _context.AppUser.GetUserAgeRestriction(userId); var seriesIds = _context.Series @@ -332,20 +340,20 @@ public class SeriesRepository : ISeriesRepository .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); - var justYear = Regex.Match(searchQuery, @"\d{4}", RegexOptions.None, Services.Tasks.Scanner.Parser.Parser.RegexTimeout).Value; + var justYear = _yearRegex.Match(searchQuery).Value; var hasYearInQuery = !string.IsNullOrEmpty(justYear); var yearComparison = hasYearInQuery ? int.Parse(justYear) : 0; result.Series = _context.Series .Where(s => libraryIds.Contains(s.LibraryId)) - .Where(s => EF.Functions.Like(s.Name, $"%{searchQuery}%") - || EF.Functions.Like(s.OriginalName, $"%{searchQuery}%") - || EF.Functions.Like(s.LocalizedName, $"%{searchQuery}%") - || EF.Functions.Like(s.NormalizedName, $"%{searchQueryNormalized}%") - || (hasYearInQuery && s.Metadata.ReleaseYear == yearComparison)) + .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))) .RestrictAgainstAgeRestriction(userRating) .Include(s => s.Library) - .OrderBy(s => s.SortName.ToLower()) + .OrderBy(s => s.SortName!.ToLower()) .AsNoTracking() .AsSplitQuery() .Take(maxRecords) @@ -362,8 +370,8 @@ public class SeriesRepository : ISeriesRepository .ToListAsync(); result.Collections = await _context.CollectionTag - .Where(c => EF.Functions.Like(c.Title, $"%{searchQuery}%") - || EF.Functions.Like(c.NormalizedTitle, $"%{searchQueryNormalized}%")) + .Where(c => (EF.Functions.Like(c.Title, $"%{searchQuery}%")) + || (EF.Functions.Like(c.NormalizedTitle, $"%{searchQueryNormalized}%"))) .Where(c => c.Promoted || isAdmin) .RestrictAgainstAgeRestriction(userRating) .OrderBy(s => s.NormalizedTitle) @@ -376,7 +384,7 @@ public class SeriesRepository : ISeriesRepository result.Persons = await _context.SeriesMetadata .Where(sm => seriesIds.Contains(sm.SeriesId)) - .SelectMany(sm => sm.People.Where(t => EF.Functions.Like(t.Name, $"%{searchQuery}%"))) + .SelectMany(sm => sm.People.Where(t => t.Name != null && EF.Functions.Like(t.Name, $"%{searchQuery}%"))) .AsSplitQuery() .Take(maxRecords) .Distinct() @@ -448,7 +456,7 @@ 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.Volumes | SeriesIncludes.Metadata) { return await _context.Series .Where(s => s.Id == seriesId) @@ -581,12 +589,11 @@ public class SeriesRepository : ISeriesRepository } } - public async Task GetSeriesCoverImageAsync(int seriesId) + public async Task GetSeriesCoverImageAsync(int seriesId) { return await _context.Series .Where(s => s.Id == seriesId) .Select(s => s.CoverImage) - .AsNoTracking() .SingleOrDefaultAsync(); } @@ -711,7 +718,7 @@ public class SeriesRepository : ISeriesRepository var cutoffProgressPoint = DateTime.Now - TimeSpan.FromDays(30); var cutoffLastAddedPoint = DateTime.Now - TimeSpan.FromDays(7); - var libraryIds = _context.Library.GetUserLibraries(userId, QueryContext.Dashboard) + var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Dashboard) .Where(id => libraryId == 0 || id == libraryId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); @@ -767,12 +774,12 @@ public class SeriesRepository : ISeriesRepository .WhereIf(hasAgeRating, s => filter.AgeRating.Contains(s.Metadata.AgeRating)) .WhereIf(hasTagsFilter, s => s.Metadata.Tags.Any(t => filter.Tags.Contains(t.Id))) .WhereIf(hasLanguageFilter, s => filter.Languages.Contains(s.Metadata.Language)) - .WhereIf(hasReleaseYearMinFilter, s => s.Metadata.ReleaseYear >= filter.ReleaseYearRange.Min) - .WhereIf(hasReleaseYearMaxFilter, s => s.Metadata.ReleaseYear <= filter.ReleaseYearRange.Max) + .WhereIf(hasReleaseYearMinFilter, s => s.Metadata.ReleaseYear >= filter.ReleaseYearRange!.Min) + .WhereIf(hasReleaseYearMaxFilter, s => s.Metadata.ReleaseYear <= filter.ReleaseYearRange!.Max) .WhereIf(hasPublicationFilter, s => filter.PublicationStatus.Contains(s.Metadata.PublicationStatus)) .WhereIf(hasSeriesNameFilter, s => EF.Functions.Like(s.Name, $"%{filter.SeriesNameQuery}%") - || EF.Functions.Like(s.OriginalName, $"%{filter.SeriesNameQuery}%") - || EF.Functions.Like(s.LocalizedName, $"%{filter.SeriesNameQuery}%")) + || EF.Functions.Like(s.OriginalName!, $"%{filter.SeriesNameQuery}%") + || EF.Functions.Like(s.LocalizedName!, $"%{filter.SeriesNameQuery}%")) .WhereIf(onlyParentSeries, s => s.RelationOf.Count == 0 || s.RelationOf.All(p => p.RelationKind == RelationKind.Prequel)) @@ -841,12 +848,12 @@ public class SeriesRepository : ISeriesRepository .WhereIf(hasAgeRating, s => filter.AgeRating.Contains(s.Metadata.AgeRating)) .WhereIf(hasTagsFilter, s => s.Metadata.Tags.Any(t => filter.Tags.Contains(t.Id))) .WhereIf(hasLanguageFilter, s => filter.Languages.Contains(s.Metadata.Language)) - .WhereIf(hasReleaseYearMinFilter, s => s.Metadata.ReleaseYear >= filter.ReleaseYearRange.Min) - .WhereIf(hasReleaseYearMaxFilter, s => s.Metadata.ReleaseYear <= filter.ReleaseYearRange.Max) + .WhereIf(hasReleaseYearMinFilter, s => s.Metadata.ReleaseYear >= filter.ReleaseYearRange!.Min) + .WhereIf(hasReleaseYearMaxFilter, s => s.Metadata.ReleaseYear <= filter.ReleaseYearRange!.Max) .WhereIf(hasPublicationFilter, s => filter.PublicationStatus.Contains(s.Metadata.PublicationStatus)) .WhereIf(hasSeriesNameFilter, s => EF.Functions.Like(s.Name, $"%{filter.SeriesNameQuery}%") - || EF.Functions.Like(s.OriginalName, $"%{filter.SeriesNameQuery}%") - || EF.Functions.Like(s.LocalizedName, $"%{filter.SeriesNameQuery}%")) + || EF.Functions.Like(s.OriginalName!, $"%{filter.SeriesNameQuery}%") + || EF.Functions.Like(s.LocalizedName!, $"%{filter.SeriesNameQuery}%")) .Where(s => userLibraries.Contains(s.LibraryId) && formats.Contains(s.Format)) .AsNoTracking(); @@ -862,7 +869,7 @@ public class SeriesRepository : ISeriesRepository { query = filter.SortOptions.SortField switch { - SortField.SortName => query.OrderBy(s => s.SortName.ToLower()), + SortField.SortName => query.OrderBy(s => s.SortName!.ToLower()), SortField.CreatedDate => query.OrderBy(s => s.Created), SortField.LastModifiedDate => query.OrderBy(s => s.LastModified), SortField.LastChapterAdded => query.OrderBy(s => s.LastChapterAdded), @@ -874,7 +881,7 @@ public class SeriesRepository : ISeriesRepository { query = filter.SortOptions.SortField switch { - SortField.SortName => query.OrderByDescending(s => s.SortName.ToLower()), + SortField.SortName => query.OrderByDescending(s => s.SortName!.ToLower()), SortField.CreatedDate => query.OrderByDescending(s => s.Created), SortField.LastModifiedDate => query.OrderByDescending(s => s.LastModified), SortField.LastChapterAdded => query.OrderByDescending(s => s.LastChapterAdded), @@ -886,7 +893,7 @@ public class SeriesRepository : ISeriesRepository return query; } - public async Task GetSeriesMetadata(int seriesId) + public async Task GetSeriesMetadata(int seriesId) { var metadataDto = await _context.SeriesMetadata .Where(metadata => metadata.SeriesId == seriesId) @@ -968,20 +975,18 @@ public class SeriesRepository : ISeriesRepository public async Task> GetAllCoverImagesAsync() { - return await _context.Series + return (await _context.Series .Select(s => s.CoverImage) .Where(t => !string.IsNullOrEmpty(t)) - .AsNoTracking() - .ToListAsync(); + .ToListAsync())!; } public async Task> GetLockedCoverImagesAsync() { - return await _context.Series + return (await _context.Series .Where(s => s.CoverImageLocked && !string.IsNullOrEmpty(s.CoverImage)) .Select(s => s.CoverImage) - .AsNoTracking() - .ToListAsync(); + .ToListAsync())!; } /// @@ -1061,31 +1066,35 @@ public class SeriesRepository : ISeriesRepository var items = (await GetRecentlyAddedChaptersQuery(userId)); if (userRating.AgeRating != AgeRating.NotApplicable) { - items = items.RestrictAgainstAgeRestriction(userRating); + items = items.RestrictAgainstAgeRestriction(userRating); } + foreach (var item in items) { - if (seriesMap.Keys.Count == pageSize) break; + if (seriesMap.Keys.Count == pageSize) break; - if (seriesMap.ContainsKey(item.SeriesName)) - { - seriesMap[item.SeriesName].Count += 1; - } - else - { - seriesMap[item.SeriesName] = new GroupedSeriesDto() - { - LibraryId = item.LibraryId, - LibraryType = item.LibraryType, - SeriesId = item.SeriesId, - SeriesName = item.SeriesName, - Created = item.Created, - Id = index, - Format = item.Format, - Count = 1, - }; - index += 1; - } + if (item.SeriesName == null) continue; + + + if (seriesMap.TryGetValue(item.SeriesName, out var value)) + { + value.Count += 1; + } + else + { + seriesMap[item.SeriesName] = new GroupedSeriesDto() + { + LibraryId = item.LibraryId, + LibraryType = item.LibraryType, + SeriesId = item.SeriesId, + SeriesName = item.SeriesName, + Created = item.Created, + Id = index, + Format = item.Format, + Count = 1, + }; + index += 1; + } } return seriesMap.Values.AsEnumerable(); @@ -1093,7 +1102,7 @@ public class SeriesRepository : ISeriesRepository public async Task> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind) { - var libraryIds = _context.Library.GetUserLibraries(userId); + var libraryIds = GetLibraryIdsForUser(userId); var userRating = await _context.AppUser.GetUserAgeRestriction(userId); var usersSeriesIds = _context.Series @@ -1120,7 +1129,7 @@ public class SeriesRepository : ISeriesRepository public async Task> GetMoreIn(int userId, int libraryId, int genreId, UserParams userParams) { - var libraryIds = _context.Library.GetUserLibraries(userId, QueryContext.Recommended) + var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) .Where(id => libraryId == 0 || id == libraryId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); @@ -1148,7 +1157,7 @@ public class SeriesRepository : ISeriesRepository /// public async Task> GetRediscover(int userId, int libraryId, UserParams userParams) { - var libraryIds = _context.Library.GetUserLibraries(userId, QueryContext.Recommended) + var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) .Where(id => libraryId == 0 || id == libraryId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); var distinctSeriesIdsWithProgress = _context.AppUserProgresses @@ -1166,9 +1175,9 @@ public class SeriesRepository : ISeriesRepository return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); } - public async Task GetSeriesForMangaFile(int mangaFileId, int userId) + public async Task GetSeriesForMangaFile(int mangaFileId, int userId) { - var libraryIds = _context.Library.GetUserLibraries(userId, QueryContext.Search); + var libraryIds = GetLibraryIdsForUser(userId, 0, QueryContext.Search); var userRating = await _context.AppUser.GetUserAgeRestriction(userId); return await _context.MangaFile @@ -1183,9 +1192,9 @@ public class SeriesRepository : ISeriesRepository .SingleOrDefaultAsync(); } - public async Task GetSeriesForChapter(int chapterId, int userId) + public async Task GetSeriesForChapter(int chapterId, int userId) { - var libraryIds = _context.Library.GetUserLibraries(userId); + var libraryIds = GetLibraryIdsForUser(userId); var userRating = await _context.AppUser.GetUserAgeRestriction(userId); return await _context.Chapter .Where(m => m.Id == chapterId) @@ -1204,11 +1213,11 @@ public class SeriesRepository : ISeriesRepository /// This will be normalized in the query /// 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) { var normalized = Services.Tasks.Scanner.Parser.Parser.NormalizePath(folder); return await _context.Series - .Where(s => s.FolderPath.Equals(normalized)) + .Where(s => s.FolderPath != null && s.FolderPath.Equals(normalized)) .Includes(includes) .SingleOrDefaultAsync(); } @@ -1220,7 +1229,8 @@ public class SeriesRepository : ISeriesRepository var userRating = await _context.AppUser.GetUserAgeRestriction(userId); return await _context.Series - .Where(s => normalizedNames.Contains(s.NormalizedName) || normalizedNames.Contains(s.NormalizedLocalizedName)) + .Where(s => normalizedNames.Contains(s.NormalizedName) || + normalizedNames.Contains(s.NormalizedLocalizedName)) .Where(s => libraryIds.Contains(s.LibraryId)) .RestrictAgainstAgeRestriction(userRating) .Includes(includes) @@ -1252,21 +1262,23 @@ 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) + public Task GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, MangaFormat format, bool withFullIncludes = true) { - var normalizedSeries = Services.Tasks.Scanner.Parser.Parser.Normalize(seriesName); - var normalizedLocalized = Services.Tasks.Scanner.Parser.Parser.Normalize(localizedName); + var normalizedSeries = seriesName.ToNormalized(); + var normalizedLocalized = localizedName.ToNormalized(); var query = _context.Series .Where(s => s.LibraryId == libraryId) .Where(s => s.Format == format && format != MangaFormat.Unknown) .Where(s => s.NormalizedName.Equals(normalizedSeries) - || (s.NormalizedLocalizedName.Equals(normalizedSeries) && s.NormalizedLocalizedName != string.Empty) - || s.OriginalName.Equals(seriesName)); + || (s.NormalizedLocalizedName == normalizedSeries) + || (s.OriginalName == seriesName)); if (!string.IsNullOrEmpty(normalizedLocalized)) { + // TODO: Apply WhereIf query = query.Where(s => - s.NormalizedName.Equals(normalizedLocalized) || s.NormalizedLocalizedName.Equals(normalizedLocalized)); + s.NormalizedName.Equals(normalizedLocalized) + || (s.NormalizedLocalizedName != null && s.NormalizedLocalizedName.Equals(normalizedLocalized))); } if (!withFullIncludes) @@ -1274,10 +1286,13 @@ public class SeriesRepository : ISeriesRepository return query.SingleOrDefaultAsync(); } + #nullable disable return query.Include(s => s.Metadata) .ThenInclude(m => m.People) + .Include(s => s.Metadata) .ThenInclude(m => m.Genres) + .Include(s => s.Library) .Include(s => s.Volumes) .ThenInclude(v => v.Chapters) @@ -1300,6 +1315,7 @@ public class SeriesRepository : ISeriesRepository .ThenInclude(c => c.Files) .AsSplitQuery() .SingleOrDefaultAsync(); + #nullable enable } @@ -1356,7 +1372,7 @@ public class SeriesRepository : ISeriesRepository public async Task> GetHighlyRated(int userId, int libraryId, UserParams userParams) { - var libraryIds = _context.Library.GetUserLibraries(userId, QueryContext.Recommended) + var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) .Where(id => libraryId == 0 || id == libraryId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); var distinctSeriesIdsWithHighRating = _context.AppUserRating @@ -1378,7 +1394,7 @@ public class SeriesRepository : ISeriesRepository public async Task> GetQuickReads(int userId, int libraryId, UserParams userParams) { - var libraryIds = _context.Library.GetUserLibraries(userId, QueryContext.Recommended) + var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) .Where(id => libraryId == 0 || id == libraryId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); var distinctSeriesIdsWithProgress = _context.AppUserProgresses @@ -1405,7 +1421,7 @@ public class SeriesRepository : ISeriesRepository public async Task> GetQuickCatchupReads(int userId, int libraryId, UserParams userParams) { - var libraryIds = _context.Library.GetUserLibraries(userId, QueryContext.Recommended) + var libraryIds = GetLibraryIdsForUser(userId, libraryId, QueryContext.Recommended) .Where(id => libraryId == 0 || id == libraryId); var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds); var distinctSeriesIdsWithProgress = _context.AppUserProgresses @@ -1581,6 +1597,7 @@ public class SeriesRepository : ISeriesRepository var map = new Dictionary>(); foreach (var series in info) { + if (series.FolderPath == null) continue; if (!map.ContainsKey(series.FolderPath)) { map.Add(series.FolderPath, new List() @@ -1603,7 +1620,7 @@ public class SeriesRepository : ISeriesRepository /// /// /// - public async Task GetMaxAgeRatingFromSeriesAsync(IEnumerable seriesIds) + public async Task GetMaxAgeRatingFromSeriesAsync(IEnumerable seriesIds) { return await _context.Series .Where(s => seriesIds.Contains(s.Id)) @@ -1612,4 +1629,32 @@ public class SeriesRepository : ISeriesRepository .OrderBy(s => s) .LastOrDefaultAsync(); } + + /// + /// Returns all library ids for a user + /// + /// + /// 0 for no library filter + /// Defaults to None - The context behind this query, so appropriate restrictions can be placed + /// + private IQueryable GetLibraryIdsForUser(int userId, int libraryId = 0, QueryContext queryContext = QueryContext.None) + { + var user = _context.AppUser + .AsSplitQuery() + .AsNoTracking() + .Where(u => u.Id == userId) + .AsSingleQuery(); + + if (libraryId == 0) + { + return user.SelectMany(l => l.Libraries) + .IsRestricted(queryContext) + .Select(lib => lib.Id); + } + + return user.SelectMany(l => l.Libraries) + .Where(lib => lib.Id == libraryId) + .IsRestricted(queryContext) + .Select(lib => lib.Id); + } } diff --git a/API/Data/Repositories/SettingsRepository.cs b/API/Data/Repositories/SettingsRepository.cs index b94204d56..c6a682391 100644 --- a/API/Data/Repositories/SettingsRepository.cs +++ b/API/Data/Repositories/SettingsRepository.cs @@ -5,7 +5,6 @@ using API.DTOs.Settings; using API.Entities; using API.Entities.Enums; using AutoMapper; -using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; @@ -44,7 +43,7 @@ public class SettingsRepository : ISettingsRepository public Task GetSettingAsync(ServerSettingKey key) { - return _context.ServerSetting.SingleOrDefaultAsync(x => x.Key == key); + return _context.ServerSetting.SingleOrDefaultAsync(x => x.Key == key)!; } public async Task> GetSettingsAsync() diff --git a/API/Data/Repositories/SiteThemeRepository.cs b/API/Data/Repositories/SiteThemeRepository.cs index 98f9c8c87..f79a84355 100644 --- a/API/Data/Repositories/SiteThemeRepository.cs +++ b/API/Data/Repositories/SiteThemeRepository.cs @@ -15,11 +15,11 @@ public interface ISiteThemeRepository void Remove(SiteTheme theme); void Update(SiteTheme siteTheme); Task> GetThemeDtos(); - Task GetThemeDto(int themeId); - Task GetThemeDtoByName(string themeName); + Task GetThemeDto(int themeId); + Task GetThemeDtoByName(string themeName); Task GetDefaultTheme(); Task> GetThemes(); - Task GetThemeById(int themeId); + Task GetThemeById(int themeId); } public class SiteThemeRepository : ISiteThemeRepository @@ -55,7 +55,7 @@ public class SiteThemeRepository : ISiteThemeRepository .ToListAsync(); } - public async Task GetThemeDtoByName(string themeName) + public async Task GetThemeDtoByName(string themeName) { return await _context.SiteTheme .Where(t => t.Name.Equals(themeName)) @@ -76,8 +76,8 @@ public class SiteThemeRepository : ISiteThemeRepository if (result == null) { return await _context.SiteTheme - .Where(t => t.NormalizedName == "dark") - .SingleOrDefaultAsync(); + .Where(t => t.NormalizedName == Seed.DefaultThemes[0].NormalizedName) + .SingleAsync(); } return result; @@ -89,14 +89,14 @@ public class SiteThemeRepository : ISiteThemeRepository .ToListAsync(); } - public async Task GetThemeById(int themeId) + public async Task GetThemeById(int themeId) { return await _context.SiteTheme .Where(t => t.Id == themeId) .SingleOrDefaultAsync(); } - public async Task GetThemeDto(int themeId) + public async Task GetThemeDto(int themeId) { return await _context.SiteTheme .Where(t => t.Id == themeId) diff --git a/API/Data/Repositories/UserRepository.cs b/API/Data/Repositories/UserRepository.cs index 0a11eccfe..8cb13cf56 100644 --- a/API/Data/Repositories/UserRepository.cs +++ b/API/Data/Repositories/UserRepository.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using API.Constants; @@ -9,11 +8,11 @@ using API.DTOs.Account; using API.DTOs.Filtering; using API.DTOs.Reader; using API.Entities; +using API.Extensions; using AutoMapper; using AutoMapper.QueryableExtensions; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; -using SixLabors.ImageSharp.PixelFormats; namespace API.Data.Repositories; @@ -38,31 +37,31 @@ public interface IUserRepository void Update(AppUserPreferences preferences); void Update(AppUserBookmark bookmark); void Add(AppUserBookmark bookmark); - public void Delete(AppUser user); + public void Delete(AppUser? user); void Delete(AppUserBookmark bookmark); Task> GetEmailConfirmedMemberDtosAsync(); Task> GetPendingMemberDtosAsync(); Task> GetAdminUsersAsync(); - Task IsUserAdminAsync(AppUser user); - Task GetUserRatingAsync(int seriesId, int userId); - Task GetPreferencesAsync(string username); + Task IsUserAdminAsync(AppUser? user); + Task GetUserRatingAsync(int seriesId, 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, FilterDto filter); Task> GetAllBookmarksAsync(); - Task GetBookmarkForPage(int page, int chapterId, int userId); - Task GetBookmarkAsync(int bookmarkId); + Task GetBookmarkForPage(int page, int chapterId, int userId); + Task GetBookmarkAsync(int bookmarkId); Task GetUserIdByApiKeyAsync(string apiKey); - Task GetUserByUsernameAsync(string username, AppUserIncludes includeFlags = AppUserIncludes.None); - Task GetUserByIdAsync(int userId, AppUserIncludes includeFlags = AppUserIncludes.None); + Task GetUserByUsernameAsync(string username, AppUserIncludes includeFlags = AppUserIncludes.None); + Task GetUserByIdAsync(int userId, AppUserIncludes includeFlags = AppUserIncludes.None); Task GetUserIdByUsernameAsync(string username); Task> GetAllBookmarksByIds(IList bookmarkIds); - Task GetUserByEmailAsync(string email); + Task GetUserByEmailAsync(string email); Task> GetAllPreferencesByThemeAsync(int themeId); Task HasAccessToLibrary(int libraryId, int userId); Task> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None); - Task GetUserByConfirmationToken(string token); + Task GetUserByConfirmationToken(string token); } public class UserRepository : IUserRepository @@ -98,8 +97,9 @@ public class UserRepository : IUserRepository _context.AppUserBookmark.Add(bookmark); } - public void Delete(AppUser user) + public void Delete(AppUser? user) { + if (user == null) return; _context.AppUser.Remove(user); } @@ -114,15 +114,12 @@ 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) { - var query = _context.Users - .Where(x => x.UserName == username); - - // TODO: Move to QueryExtensions - query = AddIncludesToQuery(query, includeFlags); - - return await query.SingleOrDefaultAsync(); + return await _context.Users + .Where(x => x.UserName == username) + .Includes(includeFlags) + .SingleOrDefaultAsync(); } /// @@ -131,14 +128,12 @@ 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) { - var query = _context.Users - .Where(x => x.Id == userId); - - query = AddIncludesToQuery(query, includeFlags); - - return await query.SingleOrDefaultAsync(); + return await _context.Users + .Where(x => x.Id == userId) + .Includes(includeFlags) + .SingleOrDefaultAsync(); } public async Task> GetAllBookmarksAsync() @@ -146,65 +141,20 @@ public class UserRepository : IUserRepository return await _context.AppUserBookmark.ToListAsync(); } - public async Task GetBookmarkForPage(int page, int chapterId, int userId) + public async Task GetBookmarkForPage(int page, int chapterId, int userId) { return await _context.AppUserBookmark .Where(b => b.Page == page && b.ChapterId == chapterId && b.AppUserId == userId) .SingleOrDefaultAsync(); } - public async Task GetBookmarkAsync(int bookmarkId) + public async Task GetBookmarkAsync(int bookmarkId) { return await _context.AppUserBookmark .Where(b => b.Id == bookmarkId) .SingleOrDefaultAsync(); } - private static IQueryable AddIncludesToQuery(IQueryable query, AppUserIncludes includeFlags) - { - if (includeFlags.HasFlag(AppUserIncludes.Bookmarks)) - { - query = query.Include(u => u.Bookmarks); - } - - if (includeFlags.HasFlag(AppUserIncludes.Progress)) - { - query = query.Include(u => u.Progresses); - } - - if (includeFlags.HasFlag(AppUserIncludes.ReadingLists)) - { - query = query.Include(u => u.ReadingLists); - } - - if (includeFlags.HasFlag(AppUserIncludes.ReadingListsWithItems)) - { - query = query.Include(u => u.ReadingLists).ThenInclude(r => r.Items); - } - - if (includeFlags.HasFlag(AppUserIncludes.Ratings)) - { - query = query.Include(u => u.Ratings); - } - - if (includeFlags.HasFlag(AppUserIncludes.UserPreferences)) - { - query = query.Include(u => u.UserPreferences); - } - - if (includeFlags.HasFlag(AppUserIncludes.WantToRead)) - { - query = query.Include(u => u.WantToRead); - } - - if (includeFlags.HasFlag(AppUserIncludes.Devices)) - { - query = query.Include(u => u.Devices); - } - - return query.AsSplitQuery(); - } - /// /// This fetches the Id for a user. Use whenever you just need an ID. @@ -233,10 +183,10 @@ public class UserRepository : IUserRepository .ToListAsync(); } - public async Task GetUserByEmailAsync(string email) + public async Task GetUserByEmailAsync(string email) { var lowerEmail = email.ToLower(); - return await _context.AppUser.SingleOrDefaultAsync(u => u.Email.ToLower().Equals(lowerEmail)); + return await _context.AppUser.SingleOrDefaultAsync(u => u.Email != null && u.Email.ToLower().Equals(lowerEmail)); } @@ -259,13 +209,15 @@ public class UserRepository : IUserRepository public async Task> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None) { - var query = AddIncludesToQuery(_context.Users.AsQueryable(), includeFlags); - return await query.ToListAsync(); + return await _context.AppUser + .Includes(includeFlags) + .ToListAsync(); } - public async Task GetUserByConfirmationToken(string token) + public async Task GetUserByConfirmationToken(string token) { - return await _context.AppUser.SingleOrDefaultAsync(u => u.ConfirmationToken.Equals(token)); + return await _context.AppUser + .SingleOrDefaultAsync(u => u.ConfirmationToken != null && u.ConfirmationToken.Equals(token)); } public async Task> GetAdminUsersAsync() @@ -273,19 +225,20 @@ public class UserRepository : IUserRepository return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole); } - public async Task IsUserAdminAsync(AppUser user) + public async Task IsUserAdminAsync(AppUser? user) { + if (user == null) return false; return await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole); } - public async Task GetUserRatingAsync(int seriesId, int userId) + public async Task GetUserRatingAsync(int seriesId, int userId) { return await _context.AppUserRating .Where(r => r.SeriesId == seriesId && r.AppUserId == userId) .SingleOrDefaultAsync(); } - public async Task GetPreferencesAsync(string username) + public async Task GetPreferencesAsync(string username) { return await _context.AppUserPreferences .Include(p => p.AppUser) @@ -342,16 +295,16 @@ public class UserRepository : IUserRepository .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); - var seriesNameQueryNormalized = Services.Tasks.Scanner.Parser.Parser.Normalize(filter.SeriesNameQuery); + var seriesNameQueryNormalized = filter.SeriesNameQuery.ToNormalized(); var filterSeriesQuery = query.Join(_context.Series, b => b.SeriesId, s => s.Id, (bookmark, series) => new { bookmark, series }) - .Where(o => EF.Functions.Like(o.series.Name, $"%{filter.SeriesNameQuery}%") - || EF.Functions.Like(o.series.OriginalName, $"%{filter.SeriesNameQuery}%") - || EF.Functions.Like(o.series.LocalizedName, $"%{filter.SeriesNameQuery}%") - || EF.Functions.Like(o.series.NormalizedName, $"%{seriesNameQueryNormalized}%") + .Where(o => (EF.Functions.Like(o.series.Name, $"%{filter.SeriesNameQuery}%")) + || (o.series.OriginalName != null && EF.Functions.Like(o.series.OriginalName, $"%{filter.SeriesNameQuery}%")) + || (o.series.LocalizedName != null && EF.Functions.Like(o.series.LocalizedName, $"%{filter.SeriesNameQuery}%")) + || (EF.Functions.Like(o.series.NormalizedName, $"%{seriesNameQueryNormalized}%")) ); query = filterSeriesQuery.Select(o => o.bookmark); @@ -370,7 +323,7 @@ public class UserRepository : IUserRepository public async Task GetUserIdByApiKeyAsync(string apiKey) { return await _context.AppUser - .Where(u => u.ApiKey.Equals(apiKey)) + .Where(u => u.ApiKey != null && u.ApiKey.Equals(apiKey)) .Select(u => u.Id) .SingleOrDefaultAsync(); } @@ -391,7 +344,7 @@ public class UserRepository : IUserRepository Email = u.Email, Created = u.Created, LastActive = u.LastActive, - Roles = u.UserRoles.Select(r => r.Role.Name).ToList(), + Roles = u.UserRoles.Select(r => r.Role.Name).ToList()!, AgeRestriction = new AgeRestrictionDto() { AgeRating = u.AgeRestriction, @@ -429,7 +382,7 @@ public class UserRepository : IUserRepository Email = u.Email, Created = u.Created, LastActive = u.LastActive, - Roles = u.UserRoles.Select(r => r.Role.Name).ToList(), + Roles = u.UserRoles.Select(r => r.Role.Name).ToList()!, AgeRestriction = new AgeRestrictionDto() { AgeRating = u.AgeRestriction, diff --git a/API/Data/Repositories/VolumeRepository.cs b/API/Data/Repositories/VolumeRepository.cs index c46110dd4..4d986ff5f 100644 --- a/API/Data/Repositories/VolumeRepository.cs +++ b/API/Data/Repositories/VolumeRepository.cs @@ -16,14 +16,14 @@ public interface IVolumeRepository void Update(Volume volume); void Remove(Volume volume); Task> GetFilesForVolume(int volumeId); - Task GetVolumeCoverImageAsync(int volumeId); + Task GetVolumeCoverImageAsync(int volumeId); Task> GetChapterIdsByVolumeIds(IReadOnlyList volumeIds); Task> GetVolumesDtoAsync(int seriesId, int userId); - Task GetVolumeAsync(int volumeId); + Task GetVolumeAsync(int volumeId); Task GetVolumeDtoAsync(int volumeId, int userId); Task> GetVolumesForSeriesAsync(IList seriesIds, bool includeChapters = false); Task> GetVolumes(int seriesId); - Task GetVolumeByIdAsync(int volumeId); + Task GetVolumeByIdAsync(int volumeId); } public class VolumeRepository : IVolumeRepository { @@ -72,12 +72,11 @@ public class VolumeRepository : IVolumeRepository /// /// /// - public async Task GetVolumeCoverImageAsync(int volumeId) + public async Task GetVolumeCoverImageAsync(int volumeId) { return await _context.Volume .Where(v => v.Id == volumeId) .Select(v => v.CoverImage) - .AsNoTracking() .SingleOrDefaultAsync(); } @@ -155,7 +154,7 @@ public class VolumeRepository : IVolumeRepository /// /// /// - public async Task GetVolumeAsync(int volumeId) + public async Task GetVolumeAsync(int volumeId) { return await _context.Volume .Include(vol => vol.Chapters) @@ -191,7 +190,7 @@ public class VolumeRepository : IVolumeRepository return volumes; } - public async Task GetVolumeByIdAsync(int volumeId) + public async Task GetVolumeByIdAsync(int volumeId) { return await _context.Volume.SingleOrDefaultAsync(x => x.Id == volumeId); } diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs index cd939c689..0cde90b6d 100644 --- a/API/Data/Seed.cs +++ b/API/Data/Seed.cs @@ -8,6 +8,7 @@ using API.Constants; using API.Entities; using API.Entities.Enums; using API.Entities.Enums.Theme; +using API.Extensions; using API.Services; using Kavita.Common; using Kavita.Common.EnvironmentInfo; @@ -29,7 +30,7 @@ public static class Seed new() { Name = "Dark", - NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize("Dark"), + NormalizedName = "Dark".ToNormalized(), Provider = ThemeProvider.System, FileName = "dark.scss", IsDefault = true, @@ -42,13 +43,13 @@ public static class Seed .GetFields(BindingFlags.Public | BindingFlags.Static) .Where(f => f.FieldType == typeof(string)) .ToDictionary(f => f.Name, - f => (string) f.GetValue(null)).Values + f => (string) f.GetValue(null)!).Values .Select(policyName => new AppRole() {Name = policyName}) .ToList(); foreach (var role in roles) { - var exists = await roleManager.RoleExistsAsync(role.Name); + var exists = await roleManager.RoleExistsAsync(role.Name!); if (!exists) { await roleManager.CreateAsync(role); diff --git a/API/Data/UnitOfWork.cs b/API/Data/UnitOfWork.cs index a62ac4552..02a089eca 100644 --- a/API/Data/UnitOfWork.cs +++ b/API/Data/UnitOfWork.cs @@ -60,7 +60,7 @@ public class UnitOfWork : IUnitOfWork public IGenreRepository GenreRepository => new GenreRepository(_context, _mapper); public ITagRepository TagRepository => new TagRepository(_context, _mapper); public ISiteThemeRepository SiteThemeRepository => new SiteThemeRepository(_context, _mapper); - public IMangaFileRepository MangaFileRepository => new MangaFileRepository(_context, _mapper); + public IMangaFileRepository MangaFileRepository => new MangaFileRepository(_context); public IDeviceRepository DeviceRepository => new DeviceRepository(_context, _mapper); /// diff --git a/API/Entities/AppRole.cs b/API/Entities/AppRole.cs index e27311027..ca46d1bb0 100644 --- a/API/Entities/AppRole.cs +++ b/API/Entities/AppRole.cs @@ -5,5 +5,5 @@ namespace API.Entities; public class AppRole : IdentityRole { - public ICollection UserRoles { get; set; } + public ICollection UserRoles { get; set; } = null!; } diff --git a/API/Entities/AppUser.cs b/API/Entities/AppUser.cs index 37db2687a..9a7405ebf 100644 --- a/API/Entities/AppUser.cs +++ b/API/Entities/AppUser.cs @@ -14,35 +14,35 @@ public class AppUser : IdentityUser, IHasConcurrencyToken public DateTime CreatedUtc { get; set; } = DateTime.UtcNow; public DateTime LastActive { get; set; } public DateTime LastActiveUtc { get; set; } - public ICollection Libraries { get; set; } - public ICollection UserRoles { get; set; } - public ICollection Progresses { get; set; } - public ICollection Ratings { get; set; } - public AppUserPreferences UserPreferences { get; set; } + public ICollection Libraries { get; set; } = null!; + public ICollection UserRoles { get; set; } = null!; + public ICollection Progresses { get; set; } = null!; + public ICollection Ratings { get; set; } = null!; + public AppUserPreferences UserPreferences { get; set; } = null!; /// /// Bookmarks associated with this User /// - public ICollection Bookmarks { get; set; } + public ICollection Bookmarks { get; set; } = null!; /// /// Reading lists associated with this user /// - public ICollection ReadingLists { get; set; } + public ICollection ReadingLists { get; set; } = null!; /// /// A list of Series the user want's to read /// - public ICollection WantToRead { get; set; } + public ICollection WantToRead { get; set; } = null!; /// /// A list of Devices which allows the user to send files to /// - public ICollection Devices { get; set; } + public ICollection Devices { get; set; } = null!; /// /// An API Key to interact with external services, like OPDS /// - public string ApiKey { get; set; } + public string? ApiKey { get; set; } /// /// The confirmation token for the user (invite). This will be set to null after the user confirms. /// - public string ConfirmationToken { get; set; } + public string? ConfirmationToken { get; set; } /// /// The highest age rating the user has access to. Not applicable for admins /// diff --git a/API/Entities/AppUserBookmark.cs b/API/Entities/AppUserBookmark.cs index f0c8dfaea..d17e8eaf0 100644 --- a/API/Entities/AppUserBookmark.cs +++ b/API/Entities/AppUserBookmark.cs @@ -23,7 +23,7 @@ public class AppUserBookmark : IEntityDate // Relationships [JsonIgnore] - public AppUser AppUser { get; set; } + public AppUser AppUser { get; set; } = null!; public int AppUserId { get; set; } public DateTime Created { get; set; } public DateTime LastModified { get; set; } diff --git a/API/Entities/AppUserPreferences.cs b/API/Entities/AppUserPreferences.cs index 60ed4a55c..828c19f72 100644 --- a/API/Entities/AppUserPreferences.cs +++ b/API/Entities/AppUserPreferences.cs @@ -1,4 +1,4 @@ -using System; +using API.Data; using API.Entities.Enums; using API.Entities.Enums.UserPreferences; @@ -75,11 +75,12 @@ public class AppUserPreferences /// Book Reader Option: What direction should the next/prev page buttons go /// public ReadingDirection BookReaderReadingDirection { get; set; } = ReadingDirection.LeftToRight; + /// /// UI Site Global Setting: The UI theme the user should use. /// /// Should default to Dark - public SiteTheme Theme { get; set; } + public required SiteTheme Theme { get; set; } = Seed.DefaultThemes[0]; /// /// Book Reader Option: The color theme to decorate the book contents /// @@ -115,6 +116,6 @@ public class AppUserPreferences /// public bool NoTransitions { get; set; } = false; - public AppUser AppUser { get; set; } + public AppUser AppUser { get; set; } = null!; public int AppUserId { get; set; } } diff --git a/API/Entities/AppUserProgress.cs b/API/Entities/AppUserProgress.cs index 3d7ca6ee4..c972af78a 100644 --- a/API/Entities/AppUserProgress.cs +++ b/API/Entities/AppUserProgress.cs @@ -37,7 +37,7 @@ public class AppUserProgress : IEntityDate /// For Book Reader, represents the nearest passed anchor on the screen that can be used to resume scroll point /// on next load /// - public string BookScrollId { get; set; } + public string? BookScrollId { get; set; } /// /// When this was first created /// @@ -54,7 +54,7 @@ public class AppUserProgress : IEntityDate /// /// Navigational Property for EF. Links to a unique AppUser /// - public AppUser AppUser { get; set; } + public AppUser AppUser { get; set; } = null!; /// /// User this progress belongs to /// diff --git a/API/Entities/AppUserRating.cs b/API/Entities/AppUserRating.cs index 54376bbd1..e4cc544df 100644 --- a/API/Entities/AppUserRating.cs +++ b/API/Entities/AppUserRating.cs @@ -11,11 +11,11 @@ public class AppUserRating /// /// A short summary the user can write when giving their review. /// - public string Review { get; set; } + public string? Review { get; set; } public int SeriesId { get; set; } // Relationships public int AppUserId { get; set; } - public AppUser AppUser { get; set; } + public AppUser AppUser { get; set; } = null!; } diff --git a/API/Entities/AppUserRole.cs b/API/Entities/AppUserRole.cs index 09ccbce6c..9ee798e6b 100644 --- a/API/Entities/AppUserRole.cs +++ b/API/Entities/AppUserRole.cs @@ -4,6 +4,6 @@ namespace API.Entities; public class AppUserRole : IdentityUserRole { - public AppUser User { get; set; } - public AppRole Role { get; set; } + public AppUser User { get; set; } = null!; + public AppRole Role { get; set; } = null!; } diff --git a/API/Entities/Chapter.cs b/API/Entities/Chapter.cs index 716a07217..37c2d1713 100644 --- a/API/Entities/Chapter.cs +++ b/API/Entities/Chapter.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using API.Entities.Enums; using API.Entities.Interfaces; using API.Parser; -using API.Services; namespace API.Entities; @@ -13,15 +12,15 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate /// /// Range of numbers. Chapter 2-4 -> "2-4". Chapter 2 -> "2". /// - public string Range { get; set; } + public required string Range { get; set; } /// /// Smallest number of the Range. Can be a partial like Chapter 4.5 /// - public string Number { get; set; } + public required string Number { get; set; } /// /// The files that represent this Chapter /// - public ICollection Files { get; set; } + public ICollection Files { get; set; } = null!; public DateTime Created { get; set; } public DateTime LastModified { get; set; } public DateTime CreatedUtc { get; set; } @@ -31,7 +30,7 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate /// Relative path to the (managed) image file representing the cover image /// /// The file is managed internally to Kavita's APPDIR - public string CoverImage { get; set; } + public string? CoverImage { get; set; } public bool CoverImageLocked { get; set; } /// /// Total number of pages in all MangaFiles @@ -44,7 +43,7 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate /// /// Used for books/specials to display custom title. For non-specials/books, will be set to /// - public string Title { get; set; } + public string? Title { get; set; } /// /// Age Rating for the issue/chapter /// @@ -62,11 +61,11 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate /// /// Summary for the Chapter/Issue /// - public string Summary { get; set; } + public string? Summary { get; set; } /// /// Language for the Chapter/Issue /// - public string Language { get; set; } + public string? Language { get; set; } /// /// Total number of issues or volumes in the series /// @@ -79,7 +78,7 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate /// /// SeriesGroup tag in ComicInfo /// - public string SeriesGroup { get; set; } + public string SeriesGroup { get; set; } = string.Empty; public string StoryArc { get; set; } = string.Empty; public string StoryArcNumber { get; set; } = string.Empty; public string AlternateNumber { get; set; } = string.Empty; @@ -118,7 +117,7 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate // Relationships - public Volume Volume { get; set; } + public Volume Volume { get; set; } = null!; public int VolumeId { get; set; } public void UpdateFrom(ParserInfo info) diff --git a/API/Entities/CollectionTag.cs b/API/Entities/CollectionTag.cs index f32e981e9..2594a9772 100644 --- a/API/Entities/CollectionTag.cs +++ b/API/Entities/CollectionTag.cs @@ -14,12 +14,12 @@ public class CollectionTag /// /// Visible title of the Tag /// - public string Title { get; set; } + public required string Title { get; set; } /// /// Absolute path to the (managed) image file /// /// The file is managed internally to Kavita's APPDIR - public string CoverImage { get; set; } + public string? CoverImage { get; set; } /// /// Denotes if the CoverImage has been overridden by the user. If so, it will not be updated during normal scan operations. /// @@ -28,18 +28,18 @@ public class CollectionTag /// /// A description of the tag /// - public string Summary { get; set; } + public string? Summary { get; set; } /// /// A normalized string used to check if the tag already exists in the DB /// - public string NormalizedTitle { get; set; } + public required string NormalizedTitle { get; set; } /// /// A promoted collection tag will allow all linked seriesMetadata's Series to show for all users. /// public bool Promoted { get; set; } - public ICollection SeriesMetadatas { get; set; } + public ICollection SeriesMetadatas { get; set; } = null!; /// /// Not Used due to not using concurrency update diff --git a/API/Entities/Device.cs b/API/Entities/Device.cs index 4e7ca32dd..ae1956f5b 100644 --- a/API/Entities/Device.cs +++ b/API/Entities/Device.cs @@ -1,8 +1,4 @@ using System; -using System.Collections; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; -using System.Net; using API.Entities.Enums.Device; using API.Entities.Interfaces; @@ -17,24 +13,24 @@ public class Device : IEntityDate /// /// Last Seen IP Address of the device /// - public string IpAddress { get; set; } + public string? IpAddress { get; set; } /// /// A name given to this device /// /// If this device is web, this will be the browser name /// Pixel 3a, John's Kindle - public string Name { get; set; } + public string? Name { get; set; } /// /// An email address associated with the device (ie Kindle). Will be used with Send to functionality /// - public string EmailAddress { get; set; } + public string? EmailAddress { get; set; } /// /// Platform (ie) Windows 10 /// public DevicePlatform Platform { get; set; } public int AppUserId { get; set; } - public AppUser AppUser { get; set; } + public AppUser AppUser { get; set; } = null!; /// diff --git a/API/Entities/Enums/ServerSettingKey.cs b/API/Entities/Enums/ServerSettingKey.cs index 084457d07..877429177 100644 --- a/API/Entities/Enums/ServerSettingKey.cs +++ b/API/Entities/Enums/ServerSettingKey.cs @@ -1,5 +1,4 @@ -using System; -using System.ComponentModel; +using System.ComponentModel; namespace API.Entities.Enums; diff --git a/API/Entities/FolderPath.cs b/API/Entities/FolderPath.cs index 98b6e503e..2d5684ba9 100644 --- a/API/Entities/FolderPath.cs +++ b/API/Entities/FolderPath.cs @@ -6,7 +6,7 @@ namespace API.Entities; public class FolderPath { public int Id { get; set; } - public string Path { get; set; } + public required string Path { get; set; } /// /// Used when scanning to see if we can skip if nothing has changed /// @@ -14,7 +14,7 @@ public class FolderPath public DateTime LastScanned { get; set; } // Relationship - public Library Library { get; set; } + public Library Library { get; set; } = null!; public int LibraryId { get; set; } public void UpdateLastScanned(DateTime? time) diff --git a/API/Entities/Genre.cs b/API/Entities/Genre.cs index 393a67860..56cb446b2 100644 --- a/API/Entities/Genre.cs +++ b/API/Entities/Genre.cs @@ -8,9 +8,9 @@ namespace API.Entities; public class Genre { public int Id { get; set; } - public string Title { get; set; } - public string NormalizedTitle { get; set; } + public required string Title { get; set; } + public required string NormalizedTitle { get; set; } - public ICollection SeriesMetadatas { get; set; } - public ICollection Chapters { get; set; } + public ICollection SeriesMetadatas { get; set; } = null!; + public ICollection Chapters { get; set; } = null!; } diff --git a/API/Entities/Library.cs b/API/Entities/Library.cs index 7b89e7152..43535ca4c 100644 --- a/API/Entities/Library.cs +++ b/API/Entities/Library.cs @@ -8,8 +8,8 @@ namespace API.Entities; public class Library : IEntityDate { public int Id { get; set; } - public string Name { get; set; } - public string CoverImage { get; set; } + public required string Name { get; set; } + public string? CoverImage { get; set; } public LibraryType Type { get; set; } /// /// If Folder Watching is enabled for this library @@ -45,9 +45,9 @@ public class Library : IEntityDate /// /// Time stored in UTC public DateTime LastScanned { get; set; } - public ICollection Folders { get; set; } - public ICollection AppUsers { get; set; } - public ICollection Series { get; set; } + public ICollection Folders { get; set; } = null!; + public ICollection AppUsers { get; set; } = null!; + public ICollection Series { get; set; } = null!; public void UpdateLastModified() { diff --git a/API/Entities/MangaFile.cs b/API/Entities/MangaFile.cs index 888548863..051555f3b 100644 --- a/API/Entities/MangaFile.cs +++ b/API/Entities/MangaFile.cs @@ -15,7 +15,7 @@ public class MangaFile : IEntityDate /// /// Absolute path to the archive file /// - public string FilePath { get; set; } + public required string FilePath { get; set; } /// /// Number of pages for the given file /// @@ -28,7 +28,7 @@ public class MangaFile : IEntityDate /// /// File extension /// - public string Extension { get; set; } + public string? Extension { get; set; } /// public DateTime Created { get; set; } /// @@ -48,7 +48,7 @@ public class MangaFile : IEntityDate // Relationship Mapping - public Chapter Chapter { get; set; } + public Chapter Chapter { get; set; } = null!; public int ChapterId { get; set; } @@ -57,6 +57,7 @@ public class MangaFile : IEntityDate /// public void UpdateLastModified() { + if (FilePath == null) return; LastModified = File.GetLastWriteTime(FilePath); LastModifiedUtc = File.GetLastWriteTimeUtc(FilePath); } diff --git a/API/Entities/Metadata/SeriesMetadata.cs b/API/Entities/Metadata/SeriesMetadata.cs index ffadac211..3d015d76e 100644 --- a/API/Entities/Metadata/SeriesMetadata.cs +++ b/API/Entities/Metadata/SeriesMetadata.cs @@ -13,7 +13,7 @@ public class SeriesMetadata : IHasConcurrencyToken public string Summary { get; set; } = string.Empty; - public ICollection CollectionTags { get; set; } + public ICollection CollectionTags { get; set; } = null!; public ICollection Genres { get; set; } = new List(); public ICollection Tags { get; set; } = new List(); @@ -71,7 +71,7 @@ public class SeriesMetadata : IHasConcurrencyToken // Relationship - public Series Series { get; set; } + public Series Series { get; set; } = null!; public int SeriesId { get; set; } /// diff --git a/API/Entities/Metadata/SeriesRelation.cs b/API/Entities/Metadata/SeriesRelation.cs index f5263f11d..7493f945b 100644 --- a/API/Entities/Metadata/SeriesRelation.cs +++ b/API/Entities/Metadata/SeriesRelation.cs @@ -11,13 +11,13 @@ public sealed class SeriesRelation public int Id { get; set; } public RelationKind RelationKind { get; set; } - public Series TargetSeries { get; set; } + public Series TargetSeries { get; set; } = null!; /// /// A is Sequel to B. In this example, TargetSeries is A. B will hold the foreign key. /// public int TargetSeriesId { get; set; } // Relationships - public Series Series { get; set; } + public Series Series { get; set; } = null!; public int SeriesId { get; set; } } diff --git a/API/Entities/Person.cs b/API/Entities/Person.cs index a7b8ea1c6..05b3cfdba 100644 --- a/API/Entities/Person.cs +++ b/API/Entities/Person.cs @@ -7,11 +7,11 @@ namespace API.Entities; public class Person { public int Id { get; set; } - public string Name { get; set; } - public string NormalizedName { get; set; } + public string? Name { get; set; } + public string? NormalizedName { get; set; } public PersonRole Role { get; set; } // Relationships - public ICollection SeriesMetadatas { get; set; } - public ICollection ChapterMetadatas { get; set; } + public ICollection SeriesMetadatas { get; set; } = null!; + public ICollection ChapterMetadatas { get; set; } = null!; } diff --git a/API/Entities/ReadingList.cs b/API/Entities/ReadingList.cs index 0d710728f..cebd2f4f3 100644 --- a/API/Entities/ReadingList.cs +++ b/API/Entities/ReadingList.cs @@ -11,12 +11,12 @@ namespace API.Entities; public class ReadingList : IEntityDate { public int Id { get; init; } - public string Title { get; set; } + public required string Title { get; set; } /// /// A normalized string used to check if the reading list already exists in the DB /// - public string NormalizedTitle { get; set; } - public string Summary { get; set; } + public string? NormalizedTitle { get; set; } + public string? Summary { get; set; } /// /// Reading lists that are promoted are only done by admins /// @@ -25,16 +25,16 @@ public class ReadingList : IEntityDate /// Absolute path to the (managed) image file /// /// The file is managed internally to Kavita's APPDIR - public string CoverImage { get; set; } + public string? CoverImage { get; set; } public bool CoverImageLocked { get; set; } /// /// The highest age rating from all Series within the reading list /// /// Introduced in v0.6 - public AgeRating AgeRating { get; set; } = AgeRating.Unknown; + public required AgeRating AgeRating { get; set; } = AgeRating.Unknown; - public ICollection Items { get; set; } + public ICollection Items { get; set; } = null!; public DateTime Created { get; set; } public DateTime LastModified { get; set; } public DateTime CreatedUtc { get; set; } @@ -42,6 +42,6 @@ public class ReadingList : IEntityDate // Relationships public int AppUserId { get; set; } - public AppUser AppUser { get; set; } + public AppUser AppUser { get; set; } = null!; } diff --git a/API/Entities/ReadingListItem.cs b/API/Entities/ReadingListItem.cs index bba133df7..c9d1de5db 100644 --- a/API/Entities/ReadingListItem.cs +++ b/API/Entities/ReadingListItem.cs @@ -12,11 +12,11 @@ public class ReadingListItem public int Order { get; set; } // Relationship - public ReadingList ReadingList { get; set; } + public ReadingList ReadingList { get; set; } = null!; public int ReadingListId { get; set; } // Keep these for easy join statements - public Series Series { get; set; } - public Volume Volume { get; set; } - public Chapter Chapter { get; set; } + public Series Series { get; set; } = null!; + public Volume Volume { get; set; } = null!; + public Chapter Chapter { get; set; } = null!; } diff --git a/API/Entities/Series.cs b/API/Entities/Series.cs index 91c469fb8..ab51ce7fd 100644 --- a/API/Entities/Series.cs +++ b/API/Entities/Series.cs @@ -12,27 +12,27 @@ public class Series : IEntityDate, IHasReadTimeEstimate /// /// The UI visible Name of the Series. This may or may not be the same as the OriginalName /// - public string Name { get; set; } + public required string Name { get; set; } /// /// Used internally for name matching. /// - public string NormalizedName { get; set; } + public required string NormalizedName { get; set; } /// /// Used internally for localized name matching. /// - public string NormalizedLocalizedName { get; set; } + public required string NormalizedLocalizedName { get; set; } /// /// The name used to sort the Series. By default, will be the same as Name. /// - public string SortName { get; set; } + public required string SortName { get; set; } /// /// Name in original language (Japanese for Manga). By default, will be same as Name. /// - public string LocalizedName { get; set; } + public required string LocalizedName { get; set; } /// /// Original Name on disk. Not exposed to UI. /// - public string OriginalName { get; set; } + public required string OriginalName { get; set; } /// /// Time of creation /// @@ -49,7 +49,7 @@ public class Series : IEntityDate, IHasReadTimeEstimate /// Absolute path to the (managed) image file /// /// The file is managed internally to Kavita's APPDIR - public string CoverImage { get; set; } + public string? CoverImage { get; set; } /// /// Denotes if the CoverImage has been overridden by the user. If so, it will not be updated during normal scan operations. /// @@ -62,7 +62,7 @@ public class Series : IEntityDate, IHasReadTimeEstimate /// Highest path (that is under library root) that contains the series. /// /// must be used before setting - public string FolderPath { get; set; } + public string? FolderPath { get; set; } /// /// Last time the folder was scanned /// @@ -96,22 +96,22 @@ public class Series : IEntityDate, IHasReadTimeEstimate public int MaxHoursToRead { get; set; } public int AvgHoursToRead { get; set; } - public SeriesMetadata Metadata { get; set; } + public SeriesMetadata Metadata { get; set; } = null!; - public ICollection Ratings { get; set; } = new List(); - public ICollection Progress { get; set; } = new List(); + public ICollection Ratings { get; set; } = null!; + public ICollection Progress { get; set; } = null!; /// /// Relations to other Series, like Sequels, Prequels, etc /// /// 1 to Many relationship - public virtual ICollection Relations { get; set; } = new List(); - public virtual ICollection RelationOf { get; set; } = new List(); + public ICollection Relations { get; set; } = null!; + public ICollection RelationOf { get; set; } = null!; // Relationships - public List Volumes { get; set; } - public Library Library { get; set; } + public List Volumes { get; set; } = null!; + public Library Library { get; set; } = null!; public int LibraryId { get; set; } public void UpdateLastFolderScanned() diff --git a/API/Entities/ServerSetting.cs b/API/Entities/ServerSetting.cs index 277bb6569..37e85efae 100644 --- a/API/Entities/ServerSetting.cs +++ b/API/Entities/ServerSetting.cs @@ -7,11 +7,11 @@ namespace API.Entities; public class ServerSetting : IHasConcurrencyToken { [Key] - public ServerSettingKey Key { get; set; } + public required ServerSettingKey Key { get; set; } /// /// The value of the Setting. Converter knows how to convert to the correct type /// - public string Value { get; set; } + public required string Value { get; set; } /// [ConcurrencyCheck] diff --git a/API/Entities/SiteTheme.cs b/API/Entities/SiteTheme.cs index 79424014f..5fea10132 100644 --- a/API/Entities/SiteTheme.cs +++ b/API/Entities/SiteTheme.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using API.Entities.Enums.Theme; using API.Entities.Interfaces; using API.Services; @@ -14,21 +13,22 @@ public class SiteTheme : IEntityDate, ITheme /// /// Name of the Theme /// - public string Name { get; set; } + public required string Name { get; set; } /// /// Normalized name for lookups /// - public string NormalizedName { get; set; } + public required string NormalizedName { get; set; } /// /// File path to the content. Stored under . /// Must be a .css file /// /// System provided themes use an alternative location as they are packaged with the app - public string FileName { get; set; } + public required string FileName { get; set; } /// /// Only one theme can have this. Will auto-set this as default for new user accounts /// public bool IsDefault { get; set; } + /// /// Where did the theme come from /// diff --git a/API/Entities/Tag.cs b/API/Entities/Tag.cs index 1676b2fd2..277422713 100644 --- a/API/Entities/Tag.cs +++ b/API/Entities/Tag.cs @@ -8,9 +8,9 @@ namespace API.Entities; public class Tag { public int Id { get; set; } - public string Title { get; set; } - public string NormalizedTitle { get; set; } + public required string Title { get; set; } + public required string NormalizedTitle { get; set; } - public ICollection SeriesMetadatas { get; set; } - public ICollection Chapters { get; set; } + public ICollection SeriesMetadatas { get; set; } = null!; + public ICollection Chapters { get; set; } = null!; } diff --git a/API/Entities/Volume.cs b/API/Entities/Volume.cs index 36747f1d7..f5239e708 100644 --- a/API/Entities/Volume.cs +++ b/API/Entities/Volume.cs @@ -11,12 +11,12 @@ public class Volume : IEntityDate, IHasReadTimeEstimate /// A String representation of the volume number. Allows for floats. /// /// For Books with Series_index, this will map to the Series Index. - public string Name { get; set; } + public required string Name { get; set; } /// /// The minimum number in the Name field in Int form /// - public int Number { get; set; } - public IList Chapters { get; set; } + public required int Number { get; set; } + public IList Chapters { get; set; } = null!; public DateTime Created { get; set; } public DateTime LastModified { get; set; } public DateTime CreatedUtc { get; set; } @@ -26,7 +26,7 @@ public class Volume : IEntityDate, IHasReadTimeEstimate /// Absolute path to the (managed) image file /// /// The file is managed internally to Kavita's APPDIR - public string CoverImage { get; set; } + public string? CoverImage { get; set; } /// /// Total pages of all chapters in this volume /// @@ -42,7 +42,7 @@ public class Volume : IEntityDate, IHasReadTimeEstimate // Relationships - public Series Series { get; set; } + public Series Series { get; set; } = null!; public int SeriesId { get; set; } } diff --git a/API/Errors/ApiException.cs b/API/Errors/ApiException.cs index d67a97f8a..8857f14a7 100644 --- a/API/Errors/ApiException.cs +++ b/API/Errors/ApiException.cs @@ -1,15 +1,9 @@ namespace API.Errors; -public class ApiException +public record ApiException { - public int Status { get; init; } - public string Message { get; init; } - public string Details { get; init; } - - public ApiException(int status, string message = null, string details = null) + public ApiException(int status, string? message = null, string? details = null) { - Status = status; - Message = message; - Details = details; + } } diff --git a/API/Extensions/ChapterListExtensions.cs b/API/Extensions/ChapterListExtensions.cs index c00fa1873..7e24a3981 100644 --- a/API/Extensions/ChapterListExtensions.cs +++ b/API/Extensions/ChapterListExtensions.cs @@ -12,7 +12,7 @@ public static class ChapterListExtensions /// /// /// - public static Chapter GetFirstChapterWithFiles(this IList chapters) + public static Chapter? GetFirstChapterWithFiles(this IEnumerable chapters) { return chapters.FirstOrDefault(c => c.Files.Any()); } @@ -24,7 +24,7 @@ public static class ChapterListExtensions /// /// /// - public static Chapter GetChapterByRange(this IList chapters, ParserInfo info) + public static Chapter? GetChapterByRange(this IEnumerable chapters, ParserInfo info) { var specialTreatment = info.IsSpecialInfo(); return specialTreatment diff --git a/API/Extensions/ConfigurationExtensions.cs b/API/Extensions/ConfigurationExtensions.cs deleted file mode 100644 index a5bfe7660..000000000 --- a/API/Extensions/ConfigurationExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.Extensions.Configuration; - -namespace API.Extensions; - -public static class ConfigurationExtensions -{ - public static int GetMaxRollingFiles(this IConfiguration config) - { - return int.Parse(config.GetSection("Logging").GetSection("File").GetSection("MaxRollingFiles").Value); - } - public static string GetLoggingFileName(this IConfiguration config) - { - return config.GetSection("Logging").GetSection("File").GetSection("Path").Value; - } -} diff --git a/API/Extensions/EnumerableExtensions.cs b/API/Extensions/EnumerableExtensions.cs index 679136efb..8dc2377df 100644 --- a/API/Extensions/EnumerableExtensions.cs +++ b/API/Extensions/EnumerableExtensions.cs @@ -19,7 +19,7 @@ public static class EnumerableExtensions /// Defaults to CurrentCulture /// /// Sorted Enumerable - public static IEnumerable OrderByNatural(this IEnumerable items, Func selector, StringComparer stringComparer = null) + public static IEnumerable OrderByNatural(this IEnumerable items, Func selector, StringComparer? stringComparer = null) { var list = items.ToList(); var maxDigits = list diff --git a/API/Extensions/HttpExtensions.cs b/API/Extensions/HttpExtensions.cs index c7820284a..4a75dfece 100644 --- a/API/Extensions/HttpExtensions.cs +++ b/API/Extensions/HttpExtensions.cs @@ -1,6 +1,4 @@ -using System; -using System.Globalization; -using System.IO; +using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; @@ -34,9 +32,7 @@ public static class HttpExtensions public static void AddCacheHeader(this HttpResponse response, byte[] content) { if (content is not {Length: > 0}) return; - using var sha1 = SHA256.Create(); - - response.Headers.Add(HeaderNames.ETag, string.Concat(sha1.ComputeHash(content).Select(x => x.ToString("X2")))); + response.Headers.Add(HeaderNames.ETag, string.Concat(SHA256.HashData(content).Select(x => x.ToString("X2")))); response.Headers.CacheControl = $"private,max-age=100"; } @@ -50,8 +46,7 @@ public static class HttpExtensions { if (filename is not {Length: > 0}) return; var hashContent = filename + File.GetLastWriteTimeUtc(filename); - using var sha1 = SHA256.Create(); - response.Headers.Add("ETag", string.Concat(sha1.ComputeHash(Encoding.UTF8.GetBytes(hashContent)).Select(x => x.ToString("X2")))); + response.Headers.Add("ETag", string.Concat(SHA256.HashData(Encoding.UTF8.GetBytes(hashContent)).Select(x => x.ToString("X2")))); if (maxAge != 10) { response.Headers.CacheControl = $"max-age={maxAge}"; diff --git a/API/Extensions/IdentityServiceExtensions.cs b/API/Extensions/IdentityServiceExtensions.cs index 6e958638a..5dc547362 100644 --- a/API/Extensions/IdentityServiceExtensions.cs +++ b/API/Extensions/IdentityServiceExtensions.cs @@ -53,7 +53,7 @@ public static class IdentityServiceExtensions options.TokenValidationParameters = new TokenValidationParameters() { ValidateIssuerSigningKey = true, - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"])), + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"]!)), ValidateIssuer = false, ValidateAudience = false, ValidIssuer = "Kavita" diff --git a/API/Extensions/QueryableExtensions.cs b/API/Extensions/QueryableExtensions.cs index dd4f5e8ad..6b8de06dd 100644 --- a/API/Extensions/QueryableExtensions.cs +++ b/API/Extensions/QueryableExtensions.cs @@ -187,6 +187,52 @@ public static class QueryableExtensions return query.AsSplitQuery(); } + public static IQueryable Includes(this IQueryable query, AppUserIncludes includeFlags) + { + if (includeFlags.HasFlag(AppUserIncludes.Bookmarks)) + { + query = query.Include(u => u.Bookmarks); + } + + if (includeFlags.HasFlag(AppUserIncludes.Progress)) + { + query = query.Include(u => u.Progresses); + } + + if (includeFlags.HasFlag(AppUserIncludes.ReadingLists)) + { + query = query.Include(u => u.ReadingLists); + } + + if (includeFlags.HasFlag(AppUserIncludes.ReadingListsWithItems)) + { + query = query.Include(u => u.ReadingLists) + .ThenInclude(r => r.Items); + } + + if (includeFlags.HasFlag(AppUserIncludes.Ratings)) + { + query = query.Include(u => u.Ratings); + } + + if (includeFlags.HasFlag(AppUserIncludes.UserPreferences)) + { + query = query.Include(u => u.UserPreferences); + } + + if (includeFlags.HasFlag(AppUserIncludes.WantToRead)) + { + query = query.Include(u => u.WantToRead); + } + + if (includeFlags.HasFlag(AppUserIncludes.Devices)) + { + query = query.Include(u => u.Devices); + } + + return query.AsSplitQuery(); + } + /// /// Applies restriction based on if the Library has restrictions (like include in search) /// diff --git a/API/Extensions/SeriesExtensions.cs b/API/Extensions/SeriesExtensions.cs index ad5ec3130..42b428579 100644 --- a/API/Extensions/SeriesExtensions.cs +++ b/API/Extensions/SeriesExtensions.cs @@ -2,62 +2,23 @@ using System.Linq; using API.Comparators; using API.Entities; -using API.Parser; -using API.Services.Tasks.Scanner; namespace API.Extensions; public static class SeriesExtensions { - /// - /// Checks against all the name variables of the Series if it matches anything in the list. This does not check against format. - /// - /// - /// - /// - public static bool NameInList(this Series series, IEnumerable list) - { - return list.Any(name => Services.Tasks.Scanner.Parser.Parser.Normalize(name) == series.NormalizedName || Services.Tasks.Scanner.Parser.Parser.Normalize(name) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.Name) - || name == series.Name || name == series.LocalizedName || name == series.OriginalName || Services.Tasks.Scanner.Parser.Parser.Normalize(name) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.OriginalName)); - } - - /// - /// Checks against all the name variables of the Series if it matches anything in the list. Includes a check against the Format of the Series - /// - /// - /// - /// - public static bool NameInList(this Series series, IEnumerable list) - { - return list.Any(name => Services.Tasks.Scanner.Parser.Parser.Normalize(name.Name) == series.NormalizedName || Services.Tasks.Scanner.Parser.Parser.Normalize(name.Name) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.Name) - || name.Name == series.Name || name.Name == series.LocalizedName || name.Name == series.OriginalName || Services.Tasks.Scanner.Parser.Parser.Normalize(name.Name) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.OriginalName) && series.Format == name.Format); - } - - /// - /// Checks against all the name variables of the Series if it matches the - /// - /// - /// - /// - public static bool NameInParserInfo(this Series series, ParserInfo info) - { - if (info == null) return false; - return Services.Tasks.Scanner.Parser.Parser.Normalize(info.Series) == series.NormalizedName || Services.Tasks.Scanner.Parser.Parser.Normalize(info.Series) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.Name) - || info.Series == series.Name || info.Series == series.LocalizedName || info.Series == series.OriginalName - || Services.Tasks.Scanner.Parser.Parser.Normalize(info.Series) == Services.Tasks.Scanner.Parser.Parser.Normalize(series.OriginalName); - } - /// /// Calculates the Cover Image for the Series /// /// /// /// This is under the assumption that the Volume already has a Cover Image calculated and set - public static string GetCoverImage(this Series series) + public static string? GetCoverImage(this Series series) { var volumes = series.Volumes ?? new List(); var firstVolume = volumes.GetCoverImage(series.Format); - string coverImage = null; + if (firstVolume == null) return null; + string? coverImage = null; var chapters = firstVolume.Chapters.OrderBy(c => double.Parse(c.Number), ChapterSortComparerZeroFirst.Default).ToList(); if (chapters.Count > 1 && chapters.Any(c => c.IsSpecial)) diff --git a/API/Extensions/StringExtensions.cs b/API/Extensions/StringExtensions.cs index 18c5ad989..1aee307c3 100644 --- a/API/Extensions/StringExtensions.cs +++ b/API/Extensions/StringExtensions.cs @@ -4,6 +4,9 @@ namespace API.Extensions; public static class StringExtensions { + // Wait for Rosyln bugfix + // [GeneratedRegex(@"(^[a-z])|\.\s+(.)", RegexOptions.ExplicitCapture | RegexOptions.Compiled)] + // private static partial Regex SentenceCaseRegex(); private static readonly Regex SentenceCaseRegex = new Regex(@"(^[a-z])|\.\s+(.)", RegexOptions.ExplicitCapture | RegexOptions.Compiled, Services.Tasks.Scanner.Parser.Parser.RegexTimeout); @@ -11,4 +14,15 @@ public static class StringExtensions { return SentenceCaseRegex.Replace(value.ToLower(), s => s.Value.ToUpper()); } + + /// + /// Apply normalization on the String + /// + /// + /// + public static string ToNormalized(this string? value) + { + if (string.IsNullOrEmpty(value)) return string.Empty; + return Services.Tasks.Scanner.Parser.Parser.Normalize(value); + } } diff --git a/API/Extensions/VolumeListExtensions.cs b/API/Extensions/VolumeListExtensions.cs index 5c9084764..0d42b15e8 100644 --- a/API/Extensions/VolumeListExtensions.cs +++ b/API/Extensions/VolumeListExtensions.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; -using API.Comparators; using API.Entities; using API.Entities.Enums; @@ -15,13 +15,16 @@ public static class VolumeListExtensions /// /// /// - public static Volume GetCoverImage(this IList volumes, MangaFormat seriesFormat) + public static Volume? GetCoverImage(this IList volumes, MangaFormat seriesFormat) { + if (volumes == null) throw new ArgumentException("Volumes cannot be null"); + if (seriesFormat == MangaFormat.Epub || seriesFormat == MangaFormat.Pdf) { return volumes.MinBy(x => x.Number); } + if (volumes.Any(x => x.Number != 0)) { return volumes.OrderBy(x => x.Number).FirstOrDefault(x => x.Number != 0); diff --git a/API/Helpers/CacheHelper.cs b/API/Helpers/CacheHelper.cs index 06a2ba764..d7e22c2e6 100644 --- a/API/Helpers/CacheHelper.cs +++ b/API/Helpers/CacheHelper.cs @@ -7,7 +7,7 @@ namespace API.Helpers; public interface ICacheHelper { - bool ShouldUpdateCoverImage(string coverPath, MangaFile firstFile, DateTime chapterCreated, + bool ShouldUpdateCoverImage(string coverPath, MangaFile? firstFile, DateTime chapterCreated, bool forceUpdate = false, bool isCoverLocked = false); @@ -37,7 +37,7 @@ public class CacheHelper : ICacheHelper /// If the user has told us to force the refresh /// If cover has been locked by user. This will force false /// - public bool ShouldUpdateCoverImage(string coverPath, MangaFile firstFile, DateTime chapterCreated, bool forceUpdate = false, + public bool ShouldUpdateCoverImage(string coverPath, MangaFile? firstFile, DateTime chapterCreated, bool forceUpdate = false, bool isCoverLocked = false) { diff --git a/API/Helpers/GenreHelper.cs b/API/Helpers/GenreHelper.cs index f6d6e85e9..6a3f40e8e 100644 --- a/API/Helpers/GenreHelper.cs +++ b/API/Helpers/GenreHelper.cs @@ -1,9 +1,10 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using API.Data; +using API.DTOs.Metadata; using API.Entities; +using API.Extensions; namespace API.Helpers; @@ -21,8 +22,8 @@ public static class GenreHelper { if (string.IsNullOrEmpty(name.Trim())) continue; - var normalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name); - var genre = allGenres.FirstOrDefault(p => p.NormalizedTitle.Equals(normalizedName)); + var normalizedName = name.ToNormalized(); + var genre = allGenres.FirstOrDefault(p => p.NormalizedTitle != null && p.NormalizedTitle.Equals(normalizedName)); if (genre == null) { genre = DbFactory.Genre(name); @@ -34,12 +35,12 @@ public static class GenreHelper } - public static void KeepOnlySameGenreBetweenLists(ICollection existingGenres, ICollection removeAllExcept, Action action = null) + public static void KeepOnlySameGenreBetweenLists(ICollection existingGenres, ICollection removeAllExcept, Action? action = null) { var existing = existingGenres.ToList(); foreach (var genre in existing) { - var existingPerson = removeAllExcept.FirstOrDefault(g => genre.NormalizedTitle.Equals(g.NormalizedTitle)); + var existingPerson = removeAllExcept.FirstOrDefault(g => genre.NormalizedTitle != null && genre.NormalizedTitle.Equals(g.NormalizedTitle)); if (existingPerson != null) continue; existingGenres.Remove(genre); action?.Invoke(genre); @@ -55,10 +56,57 @@ public static class GenreHelper public static void AddGenreIfNotExists(ICollection metadataGenres, Genre genre) { var existingGenre = metadataGenres.FirstOrDefault(p => - p.NormalizedTitle == Services.Tasks.Scanner.Parser.Parser.Normalize(genre.Title)); + p.NormalizedTitle == genre.Title?.ToNormalized()); if (existingGenre == null) { metadataGenres.Add(genre); } } + + + + public static void UpdateGenreList(ICollection? tags, Series series, + IReadOnlyCollection allTags, Action handleAdd, Action onModified) + { + if (tags == null) return; + var isModified = false; + // I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different + var existingTags = series.Metadata.Genres.ToList(); + foreach (var existing in existingTags) + { + // NOTE: Why don't I use a NormalizedName here (outside of memory pressure from string creation)? + if (tags.SingleOrDefault(t => t.Id == existing.Id) == null) + { + // Remove tag + series.Metadata.Genres.Remove(existing); + isModified = true; + } + } + + // At this point, all tags that aren't in dto have been removed. + foreach (var tagTitle in tags.Select(t => t.Title)) + { + var normalizedTitle = tagTitle.ToNormalized(); + var existingTag = allTags.SingleOrDefault(t => t.NormalizedTitle == normalizedTitle); + if (existingTag != null) + { + if (series.Metadata.Genres.All(t => t.NormalizedTitle != normalizedTitle)) + { + handleAdd(existingTag); + isModified = true; + } + } + else + { + // Add new tag + handleAdd(DbFactory.Genre(tagTitle)); + isModified = true; + } + } + + if (isModified) + { + onModified(); + } + } } diff --git a/API/Helpers/ParserInfoHelpers.cs b/API/Helpers/ParserInfoHelpers.cs index c303fd2fb..b2584214f 100644 --- a/API/Helpers/ParserInfoHelpers.cs +++ b/API/Helpers/ParserInfoHelpers.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using API.Entities; using API.Entities.Enums; +using API.Extensions; using API.Parser; using API.Services.Tasks.Scanner; @@ -22,14 +23,13 @@ public static class ParserInfoHelpers foreach (var pSeries in parsedSeries.Keys) { var name = pSeries.Name; - var normalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name); + var normalizedName = name.ToNormalized(); - //if (series.NameInParserInfo(pSeries.)) if (normalizedName == series.NormalizedName || - normalizedName == Services.Tasks.Scanner.Parser.Parser.Normalize(series.Name) || + normalizedName == series.Name.ToNormalized() || name == series.Name || name == series.LocalizedName || name == series.OriginalName || - normalizedName == Services.Tasks.Scanner.Parser.Parser.Normalize(series.OriginalName)) + normalizedName == series.OriginalName?.ToNormalized()) { format = pSeries.Format; if (format == series.Format) diff --git a/API/Helpers/PersonHelper.cs b/API/Helpers/PersonHelper.cs index adcdd4b08..89d6177f2 100644 --- a/API/Helpers/PersonHelper.cs +++ b/API/Helpers/PersonHelper.cs @@ -3,8 +3,10 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using API.Data; +using API.DTOs; using API.Entities; using API.Entities.Enums; +using API.Extensions; namespace API.Helpers; @@ -26,9 +28,9 @@ public static class PersonHelper foreach (var name in names) { - var normalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name); + var normalizedName = name.ToNormalized(); var person = allPeopleTypeRole.FirstOrDefault(p => - p.NormalizedName.Equals(normalizedName)); + p.NormalizedName != null && p.NormalizedName.Equals(normalizedName)); if (person == null) { person = DbFactory.Person(name, role); @@ -47,7 +49,7 @@ public static class PersonHelper /// People from metadata /// Role to filter on /// Callback which will be executed for each person removed - public static void RemovePeople(ICollection existingPeople, IEnumerable people, PersonRole role, Action action = null) + public static void RemovePeople(ICollection existingPeople, IEnumerable people, PersonRole role, Action? action = null) { var normalizedPeople = people.Select(Services.Tasks.Scanner.Parser.Parser.Normalize).ToList(); if (normalizedPeople.Count == 0) @@ -78,12 +80,12 @@ public static class PersonHelper /// /// /// Callback for all entities that should be removed - public static void KeepOnlySamePeopleBetweenLists(IEnumerable existingPeople, ICollection removeAllExcept, Action action = null) + public static void KeepOnlySamePeopleBetweenLists(IEnumerable existingPeople, ICollection removeAllExcept, Action? action = null) { foreach (var person in existingPeople) { var existingPerson = removeAllExcept - .FirstOrDefault(p => p.Role == person.Role && person.NormalizedName.Equals(p.NormalizedName)); + .FirstOrDefault(p => person.NormalizedName != null && p.Role == person.Role && person.NormalizedName.Equals(p.NormalizedName)); if (existingPerson == null) { action?.Invoke(person); @@ -99,7 +101,7 @@ public static class PersonHelper public static void AddPersonIfNotExists(ICollection metadataPeople, Person person) { var existingPerson = metadataPeople.SingleOrDefault(p => - p.NormalizedName == Services.Tasks.Scanner.Parser.Parser.Normalize(person.Name) && p.Role == person.Role); + p.NormalizedName == person.Name?.ToNormalized() && p.Role == person.Role); if (existingPerson == null) { metadataPeople.Add(person); @@ -114,10 +116,53 @@ public static class PersonHelper public static void AddPersonIfNotExists(BlockingCollection metadataPeople, Person person) { var existingPerson = metadataPeople.SingleOrDefault(p => - p.NormalizedName == Services.Tasks.Scanner.Parser.Parser.Normalize(person.Name) && p.Role == person.Role); + p.NormalizedName == person.Name?.ToNormalized() && p.Role == person.Role); if (existingPerson == null) { metadataPeople.Add(person); } } + + public static void UpdatePeopleList(PersonRole role, ICollection? tags, Series series, IReadOnlyCollection allTags, + Action handleAdd, Action onModified) + { + if (tags == null) return; + var isModified = false; + // I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different + var existingTags = series.Metadata.People.Where(p => p.Role == role).ToList(); + foreach (var existing in existingTags) + { + if (tags.SingleOrDefault(t => t.Id == existing.Id) == null) // This needs to check against role + { + // Remove tag + series.Metadata.People.Remove(existing); + isModified = true; + } + } + + // At this point, all tags that aren't in dto have been removed. + foreach (var tag in tags) + { + var existingTag = allTags.SingleOrDefault(t => t.Name == tag.Name && t.Role == tag.Role); + if (existingTag != null) + { + if (series.Metadata.People.Where(t => t.Role == tag.Role).All(t => t.Name != null && !t.Name.Equals(tag.Name))) + { + handleAdd(existingTag); + isModified = true; + } + } + else + { + // Add new tag + handleAdd(DbFactory.Person(tag.Name, role)); + isModified = true; + } + } + + if (isModified) + { + onModified(); + } + } } diff --git a/API/Helpers/ReadingListHelper.cs b/API/Helpers/ReadingListHelper.cs new file mode 100644 index 000000000..5f282702b --- /dev/null +++ b/API/Helpers/ReadingListHelper.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/API/Helpers/SQLHelper.cs b/API/Helpers/SQLHelper.cs index dd56a288b..575ba8c77 100644 --- a/API/Helpers/SQLHelper.cs +++ b/API/Helpers/SQLHelper.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; -using API.DTOs; using Microsoft.EntityFrameworkCore; namespace API.Helpers; diff --git a/API/Helpers/SeriesHelper.cs b/API/Helpers/SeriesHelper.cs index b30969805..2b520fb7e 100644 --- a/API/Helpers/SeriesHelper.cs +++ b/API/Helpers/SeriesHelper.cs @@ -2,6 +2,7 @@ using System.Linq; using API.Entities; using API.Entities.Enums; +using API.Extensions; using API.Services.Tasks.Scanner; namespace API.Helpers; @@ -16,9 +17,10 @@ public static class SeriesHelper /// public static bool FindSeries(Series series, ParsedSeries parsedInfoKey) { - return (series.NormalizedName.Equals(parsedInfoKey.NormalizedName) || - Services.Tasks.Scanner.Parser.Parser.Normalize(series.LocalizedName).Equals(parsedInfoKey.NormalizedName) || - Services.Tasks.Scanner.Parser.Parser.Normalize(series.OriginalName).Equals(parsedInfoKey.NormalizedName)) + 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/TagHelper.cs b/API/Helpers/TagHelper.cs index 4844f9587..125f12883 100644 --- a/API/Helpers/TagHelper.cs +++ b/API/Helpers/TagHelper.cs @@ -3,7 +3,9 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using API.Data; +using API.DTOs.Metadata; using API.Entities; +using API.Extensions; namespace API.Helpers; @@ -22,7 +24,7 @@ public static class TagHelper if (string.IsNullOrEmpty(name.Trim())) continue; var added = false; - var normalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name); + var normalizedName = name.ToNormalized(); var genre = allTags.FirstOrDefault(p => p.NormalizedTitle.Equals(normalizedName)); @@ -37,7 +39,7 @@ public static class TagHelper } } - public static void KeepOnlySameTagBetweenLists(ICollection existingTags, ICollection removeAllExcept, Action action = null) + public static void KeepOnlySameTagBetweenLists(ICollection existingTags, ICollection removeAllExcept, Action? action = null) { var existing = existingTags.ToList(); foreach (var genre in existing) @@ -58,7 +60,7 @@ public static class TagHelper public static void AddTagIfNotExists(ICollection metadataTags, Tag tag) { var existingGenre = metadataTags.FirstOrDefault(p => - p.NormalizedTitle == Services.Tasks.Scanner.Parser.Parser.Normalize(tag.Title)); + p.NormalizedTitle == tag.Title.ToNormalized()); if (existingGenre == null) { metadataTags.Add(tag); @@ -68,7 +70,7 @@ public static class TagHelper public static void AddTagIfNotExists(BlockingCollection metadataTags, Tag tag) { var existingGenre = metadataTags.FirstOrDefault(p => - p.NormalizedTitle == Services.Tasks.Scanner.Parser.Parser.Normalize(tag.Title)); + p.NormalizedTitle == tag.Title.ToNormalized()); if (existingGenre == null) { metadataTags.Add(tag); @@ -83,7 +85,7 @@ public static class TagHelper /// Tags from metadata /// Remove external tags? /// Callback which will be executed for each tag removed - public static void RemoveTags(ICollection existingTags, IEnumerable tags, Action action = null) + public static void RemoveTags(ICollection existingTags, IEnumerable tags, Action? action = null) { var normalizedTags = tags.Select(Services.Tasks.Scanner.Parser.Parser.Normalize).ToList(); foreach (var person in normalizedTags) @@ -96,5 +98,47 @@ public static class TagHelper } } + + public static void UpdateTagList(ICollection? tags, Series series, IReadOnlyCollection allTags, Action handleAdd, Action onModified) + { + if (tags == null) return; + + var isModified = false; + // I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different + var existingTags = series.Metadata.Tags.ToList(); + foreach (var existing in existingTags.Where(existing => tags.SingleOrDefault(t => t.Id == existing.Id) == null)) + { + // Remove tag + series.Metadata.Tags.Remove(existing); + isModified = true; + } + + // At this point, all tags that aren't in dto have been removed. + foreach (var tagTitle in tags.Select(t => t.Title)) + { + var normalizedTitle = tagTitle.ToNormalized(); + var existingTag = allTags.SingleOrDefault(t => t.NormalizedTitle.Equals(normalizedTitle)); + if (existingTag != null) + { + if (series.Metadata.Tags.All(t => t.NormalizedTitle != normalizedTitle)) + { + + handleAdd(existingTag); + isModified = true; + } + } + else + { + // Add new tag + handleAdd(DbFactory.Tag(tagTitle)); + isModified = true; + } + } + + if (isModified) + { + onModified(); + } + } } diff --git a/API/Logging/LogLevelOptions.cs b/API/Logging/LogLevelOptions.cs index dc927f5e9..a175c0978 100644 --- a/API/Logging/LogLevelOptions.cs +++ b/API/Logging/LogLevelOptions.cs @@ -1,11 +1,4 @@ -using System; -using System.IO; -using System.Net; -using API.Services; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; -using Serilog; +using Serilog; using Serilog.Core; using Serilog.Events; using Serilog.Formatting.Display; diff --git a/API/Program.cs b/API/Program.cs index 8ecc83d27..2d3d83a3e 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.IO.Abstractions; using System.Linq; using System.Security.Cryptography; @@ -11,7 +10,6 @@ using API.Logging; using API.Services; using API.SignalR; using Kavita.Common; -using Kavita.Common.EnvironmentInfo; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Server.Kestrel.Core; @@ -43,7 +41,8 @@ public class Program .Information() .CreateBootstrapLogger(); - var directoryService = new DirectoryService(null, new FileSystem()); + var directoryService = new DirectoryService(null!, new FileSystem()); + // Before anything, check if JWT has been generated properly or if user still has default if (!Configuration.CheckIfJwtTokenSet() && Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != Environments.Development) @@ -126,7 +125,7 @@ public class Program private static async Task GetMigrationDirectory(DataContext context, IDirectoryService directoryService) { - string currentVersion = null; + string? currentVersion = null; try { if (!await context.ServerSetting.AnyAsync()) return "vUnknown"; @@ -175,7 +174,7 @@ public class Program var ipAddresses = Configuration.IpAddresses; if (string.IsNullOrEmpty(ipAddresses)) { - opts.ListenAnyIP(HttpPort, options => { options.Protocols = HttpProtocols.Http1AndHttp2; }); + opts.ListenAnyIP(HttpPort, options => { options.Protocols = HttpProtocols.Http1AndHttp2AndHttp3; }); } else { @@ -183,7 +182,7 @@ public class Program { try { var address = System.Net.IPAddress.Parse(ipAddress.Trim()); - opts.Listen(address, HttpPort, options => { options.Protocols = HttpProtocols.Http1AndHttp2; }); + opts.Listen(address, HttpPort, options => { options.Protocols = HttpProtocols.Http1AndHttp2AndHttp3; }); } catch(Exception ex) { diff --git a/API/Services/AccountService.cs b/API/Services/AccountService.cs index 28ac98cf3..2ee6982d7 100644 --- a/API/Services/AccountService.cs +++ b/API/Services/AccountService.cs @@ -21,8 +21,9 @@ public interface IAccountService Task> ValidatePassword(AppUser user, string password); Task> ValidateUsername(string username); Task> ValidateEmail(string email); - Task HasBookmarkPermission(AppUser user); - Task HasDownloadPermission(AppUser user); + Task HasBookmarkPermission(AppUser? user); + Task HasDownloadPermission(AppUser? user); + Task HasChangeRestrictionRole(AppUser? user); Task CheckIfAccessible(HttpRequest request); Task GenerateEmailLink(HttpRequest request, string token, string routePart, string email, bool withHost = true); } @@ -137,8 +138,9 @@ public class AccountService : IAccountService /// /// /// - public async Task HasBookmarkPermission(AppUser user) + 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); } @@ -148,8 +150,9 @@ public class AccountService : IAccountService /// /// /// - public async Task HasDownloadPermission(AppUser user) + 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); } @@ -159,8 +162,9 @@ public class AccountService : IAccountService /// /// /// - public async Task HasChangeRestrictionRole(AppUser user) + public async Task HasChangeRestrictionRole(AppUser? user) { + if (user == null) return false; var roles = await _userManager.GetRolesAsync(user); return roles.Contains(PolicyConstants.ChangePasswordRole) || roles.Contains(PolicyConstants.AdminRole); } diff --git a/API/Services/ArchiveService.cs b/API/Services/ArchiveService.cs index 751f9a303..d07e6e9a4 100644 --- a/API/Services/ArchiveService.cs +++ b/API/Services/ArchiveService.cs @@ -22,7 +22,7 @@ public interface IArchiveService int GetNumberOfPagesFromArchive(string archivePath); string GetCoverImage(string archivePath, string fileName, string outputDirectory, bool saveAsWebP = false); bool IsValidArchive(string archivePath); - ComicInfo GetComicInfo(string archivePath); + ComicInfo? GetComicInfo(string archivePath); ArchiveLibrary CanOpen(string archivePath); bool ArchiveNeedsFlattening(ZipArchive archive); /// @@ -129,7 +129,7 @@ public class ArchiveService : IArchiveService /// /// /// Entry name of match, null if no match - public static string FindFolderEntry(IEnumerable entryFullNames) + 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))) @@ -163,7 +163,7 @@ public class ArchiveService : IArchiveService // Check the first folder and sort within that to see if we can find a file, else fallback to first file with basic sort. // Get first folder, then sort within that - var firstDirectoryFile = fullNames.OrderByNatural(Path.GetDirectoryName).FirstOrDefault(); + var firstDirectoryFile = fullNames.OrderByNatural(Path.GetDirectoryName!).FirstOrDefault(); if (!string.IsNullOrEmpty(firstDirectoryFile)) { var firstDirectory = Path.GetDirectoryName(firstDirectoryFile); @@ -249,7 +249,7 @@ public class ArchiveService : IArchiveService /// /// /// - public static string FindCoverImageFilename(string archivePath, IEnumerable entryNames) + public static string? FindCoverImageFilename(string archivePath, IEnumerable entryNames) { var entryName = FindFolderEntry(entryNames) ?? FirstFileEntry(entryNames, Path.GetFileName(archivePath)); return entryName; @@ -281,7 +281,7 @@ public class ArchiveService : IArchiveService var dateString = DateTime.UtcNow.ToShortDateString().Replace("/", "_"); var tempLocation = Path.Join(_directoryService.TempDirectory, $"{tempFolder}_{dateString}"); - var potentialExistingFile = _directoryService.FileSystem.FileInfo.FromFileName(Path.Join(_directoryService.TempDirectory, $"kavita_{tempFolder}_{dateString}.zip")); + 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 @@ -331,8 +331,9 @@ public class ArchiveService : IArchiveService return false; } - private static bool IsComicInfoArchiveEntry(string fullName, string name) + private static bool IsComicInfoArchiveEntry(string? fullName, string name) { + if (fullName == null) return false; return !Tasks.Scanner.Parser.Parser.HasBlacklistedFolderInPath(fullName) && name.EndsWith(ComicInfoFilename, StringComparison.OrdinalIgnoreCase) && !name.StartsWith(Tasks.Scanner.Parser.Parser.MacOsMetadataFileStartsWith); @@ -364,7 +365,7 @@ public class ArchiveService : IArchiveService { using var stream = entry.Open(); var serializer = new XmlSerializer(typeof(ComicInfo)); - var info = (ComicInfo) serializer.Deserialize(stream); + var info = (ComicInfo?) serializer.Deserialize(stream); ComicInfo.CleanComicInfo(info); return info; } @@ -382,7 +383,7 @@ public class ArchiveService : IArchiveService { using var stream = entry.OpenEntryStream(); var serializer = new XmlSerializer(typeof(ComicInfo)); - var info = (ComicInfo) serializer.Deserialize(stream); + var info = (ComicInfo?) serializer.Deserialize(stream); ComicInfo.CleanComicInfo(info); return info; } diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index 76fb0068d..cdccf10ab 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -33,8 +33,17 @@ public interface IBookService { int GetNumberOfPages(string filePath); string GetCoverImage(string fileFilePath, string fileName, string outputDirectory, bool saveAsWebP = false); - ComicInfo GetComicInfo(string filePath); - ParserInfo ParseInfo(string filePath); + 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 an target directory /// @@ -64,6 +73,27 @@ public class BookService : IBookService } }; + // Use when Rosyln fixed + // [GeneratedRegex(@"/\*[\d\D]*?\*/", RegexOptions.Compiled)] + // private static partial Regex CssComment(); + // + // [GeneratedRegex(@"[a-zA-Z]+#", RegexOptions.Compiled)] + // private static partial Regex WhiteSpace1(); + // [GeneratedRegex(@"[\n\r]+\s*", RegexOptions.Compiled)] + // private static partial Regex WhiteSpace2(); + // [GeneratedRegex(@"\s+", RegexOptions.Compiled)] + // private static partial Regex WhiteSpace3(); + // [GeneratedRegex(@"\s?([:,;{}])\s?", RegexOptions.Compiled)] + // private static partial Regex WhiteSpace4(); + // [GeneratedRegex(@"([\s:]0)(px|pt|%|em)", RegexOptions.Compiled)] + // private static partial Regex UnitPadding(); + // + // [GeneratedRegex(@")", RegexOptions.Compiled)] + // private static partial Regex StartingScriptTag(); + // [GeneratedRegex(@")", RegexOptions.Compiled)] + // private static partial Regex StartingTitleTag(); + + public BookService(ILogger logger, IDirectoryService directoryService, IImageService imageService) { _logger = logger; @@ -163,6 +193,7 @@ public class BookService : IBookService // @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; var importBuilder = new StringBuilder(); + //foreach (Match match in Tasks.Scanner.Parser.Parser.CssImportUrlRegex().Matches(stylesheetHtml)) foreach (Match match in Tasks.Scanner.Parser.Parser.CssImportUrlRegex.Matches(stylesheetHtml)) { if (!match.Success) continue; @@ -200,7 +231,7 @@ public class BookService : IBookService foreach (var styleRule in stylesheet.StyleRules) { if (styleRule.Selector.Text == CssScopeClass) continue; - if (styleRule.Selector.Text.Contains(",")) + if (styleRule.Selector.Text.Contains(',')) { styleRule.Text = styleRule.Text.Replace(styleRule.SelectorText, string.Join(", ", @@ -214,6 +245,7 @@ public class BookService : IBookService private static void EscapeCssImportReferences(ref string stylesheetHtml, string apiBase, string prepend) { + //foreach (Match match in Tasks.Scanner.Parser.Parser.CssImportUrlRegex().Matches(stylesheetHtml)) foreach (Match match in Tasks.Scanner.Parser.Parser.CssImportUrlRegex.Matches(stylesheetHtml)) { if (!match.Success) continue; @@ -224,6 +256,7 @@ public class BookService : IBookService private static void EscapeFontFamilyReferences(ref string stylesheetHtml, string apiBase, string prepend) { + //foreach (Match match in Tasks.Scanner.Parser.Parser.FontSrcUrlRegex().Matches(stylesheetHtml)) foreach (Match match in Tasks.Scanner.Parser.Parser.FontSrcUrlRegex.Matches(stylesheetHtml)) { if (!match.Success) continue; @@ -234,6 +267,7 @@ public class BookService : IBookService private static void EscapeCssImageReferences(ref string stylesheetHtml, string apiBase, EpubBookRef book) { + //var matches = Tasks.Scanner.Parser.Parser.CssImageUrlRegex().Matches(stylesheetHtml); var matches = Tasks.Scanner.Parser.Parser.CssImageUrlRegex.Matches(stylesheetHtml); foreach (Match match in matches) { @@ -260,7 +294,7 @@ public class BookService : IBookService foreach (var image in images) { - string key = null; + string? key = null; if (image.Attributes["src"] != null) { key = "src"; @@ -388,7 +422,7 @@ public class BookService : IBookService } } - public ComicInfo GetComicInfo(string filePath) + public ComicInfo? GetComicInfo(string filePath) { if (!IsValidFile(filePath) || Tasks.Scanner.Parser.Parser.IsPdf(filePath)) return null; @@ -474,7 +508,7 @@ public class BookService : IBookService return null; } - + private static (int year, int month, int day) GetPublicationDate(string publicationDate) { var dateParsed = DateTime.TryParse(publicationDate, out var date); @@ -496,23 +530,19 @@ public class BookService : IBookService return (year, month, day); } -#nullable enable private static string ValidateLanguage(string? language) { if (string.IsNullOrEmpty(language)) return string.Empty; try { - CultureInfo.GetCultureInfo(language); + return CultureInfo.GetCultureInfo(language).ToString(); } catch (Exception) { return string.Empty; } - - return language; } - #nullable disable private bool IsValidFile(string filePath) { @@ -553,6 +583,8 @@ public class BookService : IBookService private static string EscapeTags(string content) { + // content = StartingScriptTag().Replace(content, ""); + // content = StartingTitleTag().Replace(content, ""); content = Regex.Replace(content, @")", "", RegexOptions.None, Tasks.Scanner.Parser.Parser.RegexTimeout); content = Regex.Replace(content, @")", "", RegexOptions.None, Tasks.Scanner.Parser.Parser.RegexTimeout); return content; @@ -588,7 +620,7 @@ public class BookService : IBookService /// /// /// - public ParserInfo ParseInfo(string filePath) + public ParserInfo? ParseInfo(string filePath) { if (!Tasks.Scanner.Parser.Parser.IsEpub(filePath)) return null; @@ -656,7 +688,7 @@ public class BookService : IBookService Edition = string.Empty, Format = MangaFormat.Epub, Filename = Path.GetFileName(filePath), - Title = specialName?.Trim(), + Title = specialName?.Trim() ?? string.Empty, FullFilePath = filePath, IsSpecial = false, Series = series.Trim(), @@ -1037,6 +1069,12 @@ public class BookService : IBookService } // Remove comments from CSS + // body = CssComment().Replace(body, string.Empty); + // + // body = WhiteSpace1().Replace(body, "#"); + // body = WhiteSpace2().Replace(body, string.Empty); + // body = WhiteSpace3().Replace(body, " "); + // body = WhiteSpace4().Replace(body, "$1"); body = Regex.Replace(body, @"/\*[\d\D]*?\*/", string.Empty, RegexOptions.None, Tasks.Scanner.Parser.Parser.RegexTimeout); body = Regex.Replace(body, @"[a-zA-Z]+#", "#", RegexOptions.None, Tasks.Scanner.Parser.Parser.RegexTimeout); @@ -1052,6 +1090,7 @@ public class BookService : IBookService //Swallow exception. Some css don't have style rules ending in ';' } + //body = UnitPadding().Replace(body, "$1"); body = Regex.Replace(body, @"([\s:]0)(px|pt|%|em)", "$1", RegexOptions.None, Tasks.Scanner.Parser.Parser.RegexTimeout); diff --git a/API/Services/BookmarkService.cs b/API/Services/BookmarkService.cs index b79f4923d..d23905d50 100644 --- a/API/Services/BookmarkService.cs +++ b/API/Services/BookmarkService.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Constants; using API.Data; using API.DTOs.Reader; using API.Entities; @@ -50,21 +49,23 @@ public class BookmarkService : IBookmarkService /// 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) { var bookmarkDirectory = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; - var bookmarkFilesToDelete = bookmarks.Select(b => Tasks.Scanner.Parser.Parser.NormalizePath( - _directoryService.FileSystem.Path.Join(bookmarkDirectory, - b.FileName))).ToList(); + var bookmarkFilesToDelete = bookmarks + .Where(b => b != null) + .Select(b => Tasks.Scanner.Parser.Parser.NormalizePath( + _directoryService.FileSystem.Path.Join(bookmarkDirectory, b!.FileName))) + .ToList(); if (bookmarkFilesToDelete.Count == 0) return; _directoryService.DeleteFiles(bookmarkFilesToDelete); // Delete any leftover folders - foreach (var directory in _directoryService.FileSystem.Directory.GetDirectories(bookmarkDirectory, "", 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) @@ -92,7 +93,7 @@ public class BookmarkService : IBookmarkService return true; } - var fileInfo = _directoryService.FileSystem.FileInfo.FromFileName(imageToBookmark); + 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); @@ -136,7 +137,6 @@ public class BookmarkService : IBookmarkService /// public async Task RemoveBookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto) { - if (userWithBookmarks.Bookmarks == null) return true; var bookmarkToDelete = userWithBookmarks.Bookmarks.SingleOrDefault(x => x.ChapterId == bookmarkDto.ChapterId && x.Page == bookmarkDto.Page); try @@ -215,6 +215,8 @@ public class BookmarkService : IBookmarkService var count = 1F; foreach (var chapter in chapters) { + if (string.IsNullOrEmpty(chapter.CoverImage)) continue; + var newFile = await SaveAsWebP(coverDirectory, chapter.CoverImage, coverDirectory); chapter.CoverImage = Path.GetFileName(newFile); _unitOfWork.ChapterRepository.Update(chapter); diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index 0d75ceb01..8ec263404 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Comparators; using API.Data; using API.DTOs.Reader; using API.Entities; @@ -25,7 +24,7 @@ public interface ICacheService /// /// 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); + Task Ensure(int chapterId, bool extractPdfToImages = false); /// /// Clears cache directory of all volumes. This can be invoked from deleting a library or a series. /// @@ -132,14 +131,21 @@ public class CacheService : ICacheService { var extractPath = GetCachePath(chapter.Id); var path = Path.Join(extractPath, _directoryService.FileSystem.Path.GetFileName(chapter.Files.First().FilePath)); - if (!(_directoryService.FileSystem.FileInfo.FromFileName(path).Exists)) + if (!(_directoryService.FileSystem.FileInfo.New(path).Exists)) { path = chapter.Files.First().FilePath; } return path; } - public async Task Ensure(int chapterId, bool extractPdfToImages = false) + + /// + /// Caches the files for the given chapter to CacheDirectory + /// + /// + /// 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) { _directoryService.ExistOrCreate(_directoryService.CacheDirectory); var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId); @@ -160,12 +166,13 @@ public class CacheService : ICacheService /// /// Defaults to false, if true, will extract the images from the PDF renderer and not move the pdf file /// - public void ExtractChapterFiles(string extractPath, IReadOnlyList files, bool extractPdfImages = false) + public void ExtractChapterFiles(string extractPath, IReadOnlyList? files, bool extractPdfImages = false) { + if (files == null) return; var removeNonImages = true; var fileCount = files.Count; var extraPath = string.Empty; - var extractDi = _directoryService.FileSystem.DirectoryInfo.FromDirectoryName(extractPath); + var extractDi = _directoryService.FileSystem.DirectoryInfo.New(extractPath); if (files.Count > 0 && files[0].Format == MangaFormat.Image) { diff --git a/API/Services/CollectionTagService.cs b/API/Services/CollectionTagService.cs index 79443f1ef..7c2f41a4c 100644 --- a/API/Services/CollectionTagService.cs +++ b/API/Services/CollectionTagService.cs @@ -3,12 +3,12 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.Data; +using API.Data.Repositories; using API.DTOs.CollectionTags; using API.Entities; using API.Entities.Metadata; using API.SignalR; using Kavita.Common; -using Microsoft.Extensions.Logging; namespace API.Services; @@ -17,10 +17,10 @@ public interface ICollectionTagService { Task TagExistsByName(string name); Task UpdateTag(CollectionTagDto dto); - Task AddTagToSeries(CollectionTag tag, IEnumerable seriesIds); - Task RemoveTagFromSeries(CollectionTag tag, IEnumerable seriesIds); + Task AddTagToSeries(CollectionTag? tag, IEnumerable seriesIds); + Task RemoveTagFromSeries(CollectionTag? tag, IEnumerable seriesIds); Task GetTagOrCreate(int tagId, string title); - void AddTagToSeriesMetadata(CollectionTag tag, SeriesMetadata metadata); + void AddTagToSeriesMetadata(CollectionTag? tag, SeriesMetadata metadata); CollectionTag CreateTag(string title); Task RemoveTagsWithoutSeries(); } @@ -93,8 +93,9 @@ public class CollectionTagService : ICollectionTagService /// A full Tag /// /// - public async Task AddTagToSeries(CollectionTag tag, IEnumerable seriesIds) + public async Task AddTagToSeries(CollectionTag? tag, IEnumerable seriesIds) { + if (tag == null) return false; var metadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIdsAsync(seriesIds); foreach (var metadata in metadatas) { @@ -112,8 +113,9 @@ public class CollectionTagService : ICollectionTagService /// /// /// - public void AddTagToSeriesMetadata(CollectionTag tag, SeriesMetadata metadata) + public void AddTagToSeriesMetadata(CollectionTag? tag, SeriesMetadata metadata) { + if (tag == null) return; metadata.CollectionTags ??= new List(); if (metadata.CollectionTags.Any(t => t.NormalizedTitle.Equals(tag.NormalizedTitle, StringComparison.InvariantCulture))) return; @@ -124,8 +126,9 @@ public class CollectionTagService : ICollectionTagService } } - public async Task RemoveTagFromSeries(CollectionTag tag, IEnumerable seriesIds) + public async Task RemoveTagFromSeries(CollectionTag? tag, IEnumerable seriesIds) { + if (tag == null) return false; foreach (var seriesIdToRemove in seriesIds) { tag.SeriesMetadatas.Remove(tag.SeriesMetadatas.Single(sm => sm.SeriesId == seriesIdToRemove)); @@ -150,7 +153,7 @@ public class CollectionTagService : ICollectionTagService /// public async Task GetTagOrCreate(int tagId, string title) { - return await _unitOfWork.CollectionTagRepository.GetFullTagAsync(tagId) ?? CreateTag(title); + return await _unitOfWork.CollectionTagRepository.GetTagAsync(tagId, CollectionTagIncludes.SeriesMetadata) ?? CreateTag(title); } /// diff --git a/API/Services/DeviceService.cs b/API/Services/DeviceService.cs index ffab5a858..2fb9768ae 100644 --- a/API/Services/DeviceService.cs +++ b/API/Services/DeviceService.cs @@ -8,7 +8,6 @@ using API.DTOs.Email; using API.Entities; using API.Entities.Enums; using API.Entities.Enums.Device; -using API.SignalR; using Kavita.Common; using Microsoft.Extensions.Logging; @@ -16,8 +15,8 @@ namespace API.Services; public interface IDeviceService { - Task Create(CreateDeviceDto dto, AppUser userWithDevices); - Task Update(UpdateDeviceDto dto, AppUser userWithDevices); + Task Create(CreateDeviceDto dto, AppUser userWithDevices); + Task Update(UpdateDeviceDto dto, AppUser userWithDevices); Task Delete(AppUser userWithDevices, int deviceId); Task SendTo(IReadOnlyList chapterIds, int deviceId); } @@ -34,13 +33,13 @@ public class DeviceService : IDeviceService _logger = logger; _emailService = emailService; } - #nullable enable + public async Task Create(CreateDeviceDto dto, AppUser userWithDevices) { try { userWithDevices.Devices ??= new List(); - var existingDevice = userWithDevices.Devices.SingleOrDefault(d => d.Name.Equals(dto.Name)); + var existingDevice = userWithDevices.Devices.SingleOrDefault(d => d.Name!.Equals(dto.Name)); if (existingDevice != null) throw new KavitaException("A device with this name already exists"); existingDevice = DbFactory.Device(dto.Name); @@ -85,7 +84,6 @@ public class DeviceService : IDeviceService return null; } - #nullable disable public async Task Delete(AppUser userWithDevices, int deviceId) { @@ -119,7 +117,7 @@ public class DeviceService : IDeviceService await _unitOfWork.CommitAsync(); var success = await _emailService.SendFilesToEmail(new SendToDto() { - DestinationEmail = device.EmailAddress, + DestinationEmail = device.EmailAddress!, FilePaths = files.Select(m => m.FilePath) }); diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 10f00e66b..4f0ee7b31 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -1,17 +1,14 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics; using System.IO; 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 Kavita.Common.Helpers; -using Microsoft.Extensions.FileSystemGlobbing; using Microsoft.Extensions.Logging; namespace API.Services; @@ -47,33 +44,25 @@ public interface IDirectoryService void ClearDirectory(string directoryPath); void ClearAndDeleteDirectory(string directoryPath); string[] GetFilesWithExtension(string path, string searchPatternExpression = ""); - bool CopyDirectoryToDirectory(string sourceDirName, string destDirName, string searchPattern = ""); - + bool CopyDirectoryToDirectory(string? sourceDirName, string destDirName, string searchPattern = ""); Dictionary FindHighestDirectoriesFromFiles(IEnumerable 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 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 GetDirectories(string folderPath, GlobMatcher? matcher); string GetParentDirectoryName(string fileOrFolder); -#nullable enable IList ScanFiles(string folderPath, GlobMatcher? matcher = null); DateTime GetLastWriteTime(string folderPath); - GlobMatcher CreateMatcherFromFile(string filePath); -#nullable disable + GlobMatcher? CreateMatcherFromFile(string filePath); } public class DirectoryService : IDirectoryService { @@ -87,6 +76,16 @@ public class DirectoryService : IDirectoryService public string BookmarkDirectory { get; } public string SiteThemeDirectory { get; } private readonly ILogger _logger; + private const int RegexTimeoutMs = 5000000; + private const RegexOptions MatchOptions = RegexOptions.Compiled | RegexOptions.IgnoreCase; + + // [GeneratedRegex(@"@eaDir|\.DS_Store|\.qpkg|__MACOSX|@Recently-Snapshot|@recycle", + // MatchOptions, matchTimeoutMilliseconds: RegexTimeoutMs)] + // private static partial Regex ExcludeDirectoriesRegex(); + // + // [GeneratedRegex(@"\(\d+\)", + // MatchOptions, matchTimeoutMilliseconds: RegexTimeoutMs)] + // private static partial Regex FileCopyAppendRegex(); private static readonly Regex ExcludeDirectories = new Regex( @"@eaDir|\.DS_Store|\.qpkg|__MACOSX|@Recently-Snapshot|@recycle|\.@__thumb", @@ -130,7 +129,7 @@ public class DirectoryService : IDirectoryService SearchOption searchOption = SearchOption.TopDirectoryOnly) { if (!FileSystem.Directory.Exists(path)) return ImmutableList.Empty; - var reSearchPattern = new Regex(searchPatternExpression, RegexOptions.IgnoreCase); + var reSearchPattern = new Regex(searchPatternExpression, RegexOptions.IgnoreCase, Tasks.Scanner.Parser.Parser.RegexTimeout); return FileSystem.Directory.EnumerateFiles(path, "*", searchOption) .Where(file => @@ -172,7 +171,7 @@ public class DirectoryService : IDirectoryService while (FileSystem.Path.GetDirectoryName(path) != Path.GetDirectoryName(root)) { - var folder = FileSystem.DirectoryInfo.FromDirectoryName(path).Name; + var folder = FileSystem.DirectoryInfo.New(path).Name; paths.Add(folder); path = path.Substring(0, path.LastIndexOf(separator)); } @@ -187,7 +186,7 @@ public class DirectoryService : IDirectoryService /// public bool Exists(string directory) { - var di = FileSystem.DirectoryInfo.FromDirectoryName(directory); + var di = FileSystem.DirectoryInfo.New(directory); return di.Exists; } @@ -230,7 +229,7 @@ public class DirectoryService : IDirectoryService { try { - var fileInfo = FileSystem.FileInfo.FromFileName(fullFilePath); + var fileInfo = FileSystem.FileInfo.New(fullFilePath); if (!fileInfo.Exists) return; ExistOrCreate(targetDirectory); @@ -250,12 +249,12 @@ public class DirectoryService : IDirectoryService /// Defaults to all files /// If was successful /// Thrown when source directory does not exist - public bool CopyDirectoryToDirectory(string sourceDirName, string destDirName, string searchPattern = "") + public bool CopyDirectoryToDirectory(string? sourceDirName, string destDirName, string searchPattern = "") { if (string.IsNullOrEmpty(sourceDirName)) return false; // Get the subdirectories for the specified directory. - var dir = FileSystem.DirectoryInfo.FromDirectoryName(sourceDirName); + var dir = FileSystem.DirectoryInfo.New(sourceDirName); if (!dir.Exists) { @@ -270,7 +269,7 @@ public class DirectoryService : IDirectoryService ExistOrCreate(destDirName); // Get the files in the directory and copy them to the new location. - var files = GetFilesWithExtension(dir.FullName, searchPattern).Select(n => FileSystem.FileInfo.FromFileName(n)); + var files = GetFilesWithExtension(dir.FullName, searchPattern).Select(n => FileSystem.FileInfo.New(n)); foreach (var file in files) { var tempPath = FileSystem.Path.Combine(destDirName, file.Name); @@ -294,7 +293,7 @@ public class DirectoryService : IDirectoryService /// public bool IsDriveMounted(string path) { - return FileSystem.DirectoryInfo.FromDirectoryName(FileSystem.Path.GetPathRoot(path) ?? string.Empty).Exists; + return FileSystem.DirectoryInfo.New(FileSystem.Path.GetPathRoot(path) ?? string.Empty).Exists; } @@ -325,7 +324,7 @@ public class DirectoryService : IDirectoryService /// Total bytes public long GetTotalSize(IEnumerable paths) { - return paths.Sum(path => FileSystem.FileInfo.FromFileName(path).Length); + return paths.Sum(path => FileSystem.FileInfo.New(path).Length); } /// @@ -335,7 +334,7 @@ public class DirectoryService : IDirectoryService /// public bool ExistOrCreate(string directoryPath) { - var di = FileSystem.DirectoryInfo.FromDirectoryName(directoryPath); + var di = FileSystem.DirectoryInfo.New(directoryPath); if (di.Exists) return true; try { @@ -356,7 +355,7 @@ public class DirectoryService : IDirectoryService { if (!FileSystem.Directory.Exists(directoryPath)) return; - var di = FileSystem.DirectoryInfo.FromDirectoryName(directoryPath); + var di = FileSystem.DirectoryInfo.New(directoryPath); ClearDirectory(directoryPath); @@ -370,7 +369,7 @@ public class DirectoryService : IDirectoryService /// public void ClearDirectory(string directoryPath) { - var di = FileSystem.DirectoryInfo.FromDirectoryName(directoryPath); + var di = FileSystem.DirectoryInfo.New(directoryPath); if (!di.Exists) return; try { @@ -401,7 +400,7 @@ public class DirectoryService : IDirectoryService public bool CopyFilesToDirectory(IEnumerable filePaths, string directoryPath, string prepend = "") { ExistOrCreate(directoryPath); - string currentFile = null; + string? currentFile = null; try { foreach (var file in filePaths) @@ -413,8 +412,8 @@ public class DirectoryService : IDirectoryService _logger.LogError("Unable to copy {File} to {DirectoryPath} as it doesn't exist", file, directoryPath); continue; } - var fileInfo = FileSystem.FileInfo.FromFileName(file); - var targetFile = FileSystem.FileInfo.FromFileName(RenameFileForCopy(file, directoryPath, prepend)); + var fileInfo = FileSystem.FileInfo.New(file); + var targetFile = FileSystem.FileInfo.New(RenameFileForCopy(file, directoryPath, prepend)); fileInfo.CopyTo(FileSystem.Path.Join(directoryPath, targetFile.Name)); } @@ -439,7 +438,7 @@ public class DirectoryService : IDirectoryService public bool CopyFilesToDirectory(IEnumerable filePaths, string directoryPath, IList newFilenames) { ExistOrCreate(directoryPath); - string currentFile = null; + string? currentFile = null; var index = 0; try { @@ -452,8 +451,8 @@ public class DirectoryService : IDirectoryService _logger.LogError("Unable to copy {File} to {DirectoryPath} as it doesn't exist", file, directoryPath); continue; } - var fileInfo = FileSystem.FileInfo.FromFileName(file); - var targetFile = FileSystem.FileInfo.FromFileName(RenameFileForCopy(newFilenames[index] + fileInfo.Extension, directoryPath)); + var fileInfo = FileSystem.FileInfo.New(file); + var targetFile = FileSystem.FileInfo.New(RenameFileForCopy(newFilenames[index] + fileInfo.Extension, directoryPath)); fileInfo.CopyTo(FileSystem.Path.Join(directoryPath, targetFile.Name)); index++; @@ -480,18 +479,20 @@ public class DirectoryService : IDirectoryService { while (true) { - var fileInfo = FileSystem.FileInfo.FromFileName(fileToCopy); + var fileInfo = FileSystem.FileInfo.New(fileToCopy); var filename = prepend + fileInfo.Name; - var targetFile = FileSystem.FileInfo.FromFileName(FileSystem.Path.Join(directoryPath, filename)); + var targetFile = FileSystem.FileInfo.New(FileSystem.Path.Join(directoryPath, filename)); if (!targetFile.Exists) { return targetFile.FullName; } var noExtension = FileSystem.Path.GetFileNameWithoutExtension(fileInfo.Name); + //if (FileCopyAppendRegex().IsMatch(noExtension)) if (FileCopyAppend.IsMatch(noExtension)) { + //var match = FileCopyAppendRegex().Match(noExtension).Value; var match = FileCopyAppend.Match(noExtension).Value; var matchNumber = match.Replace("(", string.Empty).Replace(")", string.Empty); noExtension = noExtension.Replace(match, $"({int.Parse(matchNumber) + 1})"); @@ -515,7 +516,7 @@ public class DirectoryService : IDirectoryService { if (!FileSystem.Directory.Exists(rootPath)) return ImmutableList.Empty; - var di = FileSystem.DirectoryInfo.FromDirectoryName(rootPath); + var di = FileSystem.DirectoryInfo.New(rootPath); var dirs = di.GetDirectories() .Where(dir => !(dir.Attributes.HasFlag(FileAttributes.Hidden) || dir.Attributes.HasFlag(FileAttributes.System))) .Select(d => new DirectoryDto() @@ -595,13 +596,13 @@ public class DirectoryService : IDirectoryService /// /// A set of glob rules that will filter directories out /// List of directory paths, empty if path doesn't exist - public IEnumerable GetDirectories(string folderPath, GlobMatcher matcher) + public IEnumerable GetDirectories(string folderPath, GlobMatcher? matcher) { if (matcher == null) return GetDirectories(folderPath); return GetDirectories(folderPath) .Where(folder => !matcher.ExcludeMatches( - $"{FileSystem.DirectoryInfo.FromDirectoryName(folder).Name}{FileSystem.Path.AltDirectorySeparatorChar}")); + $"{FileSystem.DirectoryInfo.New(folder).Name}{FileSystem.Path.AltDirectorySeparatorChar}")); } /// @@ -681,7 +682,7 @@ public class DirectoryService : IDirectoryService { var foundFiles = GetFilesWithCertainExtensions(folderPath, Tasks.Scanner.Parser.Parser.SupportedExtensions) - .Where(file => !matcher.ExcludeMatches(FileSystem.FileInfo.FromFileName(file).Name)); + .Where(file => !matcher.ExcludeMatches(FileSystem.FileInfo.New(file).Name)); files.AddRange(foundFiles); } @@ -707,7 +708,7 @@ public class DirectoryService : IDirectoryService /// /// /// - public GlobMatcher CreateMatcherFromFile(string filePath) + public GlobMatcher? CreateMatcherFromFile(string filePath) { if (!FileSystem.File.Exists(filePath)) { @@ -831,7 +832,7 @@ public class DirectoryService : IDirectoryService { try { - FileSystem.FileInfo.FromFileName(file).Delete(); + FileSystem.FileInfo.New(file).Delete(); } catch (Exception) { @@ -934,7 +935,7 @@ public class DirectoryService : IDirectoryService { if (string.IsNullOrEmpty(directoryName) || !FileSystem.Directory.Exists(directoryName)) return; - var directory = FileSystem.DirectoryInfo.FromDirectoryName(directoryName); + var directory = FileSystem.DirectoryInfo.New(directoryName); var index = 0; FlattenDirectory(directory, directory, ref index); diff --git a/API/Services/DownloadService.cs b/API/Services/DownloadService.cs index a89a0988f..6ff152df9 100644 --- a/API/Services/DownloadService.cs +++ b/API/Services/DownloadService.cs @@ -2,9 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading.Tasks; using API.Entities; -using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.StaticFiles; namespace API.Services; @@ -53,7 +51,7 @@ public class DownloadService : IDownloadService }; } - return contentType; + return contentType!; } diff --git a/API/Services/EmailService.cs b/API/Services/EmailService.cs index 32823c178..085f4567e 100644 --- a/API/Services/EmailService.cs +++ b/API/Services/EmailService.cs @@ -78,13 +78,13 @@ public class EmailService : IEmailService public async Task IsDefaultEmailService() { - return (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value + return (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl))!.Value! .Equals(DefaultApiUrl); } public async Task SendEmailChangeEmail(ConfirmationEmailDto data) { - var emailLink = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value; + var emailLink = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl))!.Value; var success = await SendEmailWithPost(emailLink + "/api/account/email-change", data); if (!success) { diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs index a3eee4178..8eb42100b 100644 --- a/API/Services/ImageService.cs +++ b/API/Services/ImageService.cs @@ -17,9 +17,10 @@ public interface IImageService /// /// base64 encoded image /// + /// Convert and save as webp /// Width of thumbnail /// File name with extension of the file. This will always write to - string CreateThumbnailFromBase64(string encodedImage, string fileName, int thumbnailWidth = 0); + string CreateThumbnailFromBase64(string encodedImage, string fileName, bool saveAsWebP = false, int thumbnailWidth = 320); string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, bool saveAsWebP = false); /// @@ -42,7 +43,6 @@ public class ImageService : IImageService public const string CollectionTagCoverImageRegex = @"tag\d+"; public const string ReadingListCoverImageRegex = @"readinglist\d+"; - /// /// Width of the Thumbnail generation /// @@ -58,8 +58,9 @@ public class ImageService : IImageService _directoryService = directoryService; } - public void ExtractImages(string fileFilePath, string targetDirectory, int fileCount = 1) + public void ExtractImages(string? fileFilePath, string targetDirectory, int fileCount = 1) { + if (string.IsNullOrEmpty(fileFilePath)) return; _directoryService.ExistOrCreate(targetDirectory); if (fileCount == 1) { @@ -67,7 +68,7 @@ public class ImageService : IImageService } else { - _directoryService.CopyDirectoryToDirectory(Path.GetDirectoryName(fileFilePath), targetDirectory, + _directoryService.CopyDirectoryToDirectory(_directoryService.FileSystem.Path.GetDirectoryName(fileFilePath), targetDirectory, Tasks.Scanner.Parser.Parser.ImageFileExtensions); } } @@ -115,7 +116,7 @@ public class ImageService : IImageService public Task ConvertToWebP(string filePath, string outputPath) { - var file = _directoryService.FileSystem.FileInfo.FromFileName(filePath); + var file = _directoryService.FileSystem.FileInfo.New(filePath); var fileName = file.Name.Replace(file.Extension, string.Empty); var outputFile = Path.Join(outputPath, fileName + ".webp"); @@ -143,12 +144,12 @@ public class ImageService : IImageService /// - public string CreateThumbnailFromBase64(string encodedImage, string fileName, int thumbnailWidth = ThumbnailWidth) + public string CreateThumbnailFromBase64(string encodedImage, string fileName, bool saveAsWebP = false, int thumbnailWidth = ThumbnailWidth) { try { - using var thumbnail = Image.ThumbnailBuffer(Convert.FromBase64String(encodedImage), ThumbnailWidth); - var filename = fileName + ".png"; + using var thumbnail = Image.ThumbnailBuffer(Convert.FromBase64String(encodedImage), thumbnailWidth); + var filename = fileName + (saveAsWebP ? ".webp" : ".png"); thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, fileName + ".png")); return filename; } diff --git a/API/Services/MetadataService.cs b/API/Services/MetadataService.cs index 833ee83c8..369d555f3 100644 --- a/API/Services/MetadataService.cs +++ b/API/Services/MetadataService.cs @@ -5,18 +5,11 @@ using System.Linq; using System.Threading.Tasks; using API.Comparators; using API.Data; -using API.Data.Metadata; -using API.Data.Repositories; -using API.Data.Scanner; -using API.DTOs.Metadata; using API.Entities; -using API.Entities.Enums; using API.Extensions; using API.Helpers; -using API.Services.Tasks.Metadata; using API.SignalR; using Hangfire; -using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; namespace API.Services; @@ -74,11 +67,13 @@ public class MetadataService : IMetadataService private Task UpdateChapterCoverImage(Chapter chapter, bool forceUpdate, bool convertToWebPOnWrite) { var firstFile = chapter.Files.MinBy(x => x.Chapter); + if (firstFile == null) return Task.FromResult(false); - if (!_cacheHelper.ShouldUpdateCoverImage(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, chapter.CoverImage), firstFile, chapter.Created, forceUpdate, chapter.CoverImageLocked)) + if (!_cacheHelper.ShouldUpdateCoverImage(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, chapter.CoverImage), + firstFile, chapter.Created, forceUpdate, chapter.CoverImageLocked)) return Task.FromResult(false); - if (firstFile == null) return Task.FromResult(false); + _logger.LogDebug("[MetadataService] Generating cover image for {File}", firstFile.FilePath); @@ -102,7 +97,7 @@ public class MetadataService : IMetadataService /// /// /// Force updating cover image even if underlying file has not been modified or chapter already has a cover image - private Task UpdateVolumeCoverImage(Volume volume, bool forceUpdate) + private Task UpdateVolumeCoverImage(Volume? volume, bool forceUpdate) { // We need to check if Volume coverImage matches first chapters if forceUpdate is false if (volume == null || !_cacheHelper.ShouldUpdateCoverImage( @@ -125,7 +120,7 @@ public class MetadataService : IMetadataService /// /// /// Force updating cover image even if underlying file has not been modified or chapter already has a cover image - private Task UpdateSeriesCoverImage(Series series, bool forceUpdate) + private Task UpdateSeriesCoverImage(Series? series, bool forceUpdate) { if (series == null) return Task.CompletedTask; @@ -199,6 +194,7 @@ public class MetadataService : IMetadataService public async Task GenerateCoversForLibrary(int libraryId, bool forceUpdate = false) { var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); + if (library == null) return; _logger.LogInformation("[MetadataService] Beginning cover generation refresh of {LibraryName}", library.Name); _updateEvents.Clear(); diff --git a/API/Services/ReaderService.cs b/API/Services/ReaderService.cs index 6997e5b27..8a2d1ab0a 100644 --- a/API/Services/ReaderService.cs +++ b/API/Services/ReaderService.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; @@ -105,6 +104,7 @@ public class ReaderService : IReaderService { var seenVolume = new Dictionary(); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); + if (series == null) throw new KavitaException("Series suddenly doesn't exist, cannot mark as read"); foreach (var chapter in chapters) { var userProgress = GetUserProgressForChapter(user, chapter); @@ -128,14 +128,14 @@ public class ReaderService : IReaderService } await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate, - MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, seriesId, chapter.VolumeId, chapter.Id, chapter.Pages)); + MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName!, seriesId, chapter.VolumeId, chapter.Id, chapter.Pages)); // Send out volume events for each distinct volume if (!seenVolume.ContainsKey(chapter.VolumeId)) { seenVolume[chapter.VolumeId] = true; await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate, - MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, seriesId, + MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName!, seriesId, chapter.VolumeId, 0, chapters.Where(c => c.VolumeId == chapter.VolumeId).Sum(c => c.Pages))); } @@ -164,14 +164,14 @@ public class ReaderService : IReaderService userProgress.VolumeId = chapter.VolumeId; await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate, - MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, userProgress.SeriesId, userProgress.VolumeId, userProgress.ChapterId, 0)); + MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName!, userProgress.SeriesId, userProgress.VolumeId, userProgress.ChapterId, 0)); // Send out volume events for each distinct volume if (!seenVolume.ContainsKey(chapter.VolumeId)) { seenVolume[chapter.VolumeId] = true; await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate, - MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, seriesId, + MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName!, seriesId, chapter.VolumeId, 0, 0)); } } @@ -184,9 +184,9 @@ public class ReaderService : IReaderService /// Must have Progresses populated /// /// - private static AppUserProgress GetUserProgressForChapter(AppUser user, Chapter chapter) + private static AppUserProgress? GetUserProgressForChapter(AppUser user, Chapter chapter) { - AppUserProgress userProgress = null; + AppUserProgress? userProgress = null; if (user.Progresses == null) { @@ -227,7 +227,7 @@ public class ReaderService : IReaderService try { - // TODO: Rewrite this code to just pull user object with progress for that particiular appuserprogress, else create it + // TODO: Rewrite this code to just pull user object with progress for that particular appuserprogress, else create it var userProgress = await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(progressDto.ChapterId, userId); @@ -237,6 +237,7 @@ public class ReaderService : IReaderService // Create a user object var userWithProgress = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.Progress); + if (userWithProgress == null) return false; userWithProgress.Progresses ??= new List(); userWithProgress.Progresses.Add(new AppUserProgress { @@ -263,7 +264,7 @@ public class ReaderService : IReaderService { var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate, - MessageFactory.UserProgressUpdateEvent(userId, user.UserName, progressDto.SeriesId, progressDto.VolumeId, progressDto.ChapterId, progressDto.PageNum)); + MessageFactory.UserProgressUpdateEvent(userId, user!.UserName!, progressDto.SeriesId, progressDto.VolumeId, progressDto.ChapterId, progressDto.PageNum)); return true; } } diff --git a/API/Services/ReadingItemService.cs b/API/Services/ReadingItemService.cs index 14ced8674..d802ac5c4 100644 --- a/API/Services/ReadingItemService.cs +++ b/API/Services/ReadingItemService.cs @@ -8,12 +8,11 @@ namespace API.Services; public interface IReadingItemService { - ComicInfo GetComicInfo(string filePath); + ComicInfo? GetComicInfo(string filePath); int GetNumberOfPages(string filePath, MangaFormat format); string GetCoverImage(string filePath, string fileName, MangaFormat format, bool saveAsWebP); void Extract(string fileFilePath, string targetDirectory, MangaFormat format, int imageCount = 1); - ParserInfo Parse(string path, string rootPath, LibraryType type); - ParserInfo ParseFile(string path, string rootPath, LibraryType type); + ParserInfo? ParseFile(string path, string rootPath, LibraryType type); } public class ReadingItemService : IReadingItemService @@ -60,7 +59,7 @@ public class ReadingItemService : IReadingItemService /// Path of a file /// /// Library type to determine parsing to perform - public ParserInfo ParseFile(string path, string rootPath, LibraryType type) + public ParserInfo? ParseFile(string path, string rootPath, LibraryType type) { var info = Parse(path, rootPath, type); if (info == null) @@ -216,7 +215,7 @@ public class ReadingItemService : IReadingItemService /// /// /// - public ParserInfo Parse(string path, string rootPath, LibraryType type) + private ParserInfo? Parse(string path, string rootPath, LibraryType type) { return Tasks.Scanner.Parser.Parser.IsEpub(path) ? _bookService.ParseInfo(path) : _defaultParser.Parse(path, rootPath, type); } diff --git a/API/Services/ReadingListService.cs b/API/Services/ReadingListService.cs index 2ba5da693..b8271f166 100644 --- a/API/Services/ReadingListService.cs +++ b/API/Services/ReadingListService.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; @@ -7,7 +6,6 @@ using System.Threading.Tasks; using API.Comparators; using API.Data; using API.Data.Repositories; -using API.DTOs; using API.DTOs.ReadingLists; using API.DTOs.ReadingLists.CBL; using API.Entities; @@ -182,6 +180,7 @@ public class ReadingListService : IReadingListService _unitOfWork.ReadingListRepository.BulkRemove(listItems); var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListId); + if (readingList == null) return true; await CalculateReadingListAgeRating(readingList); if (!_unitOfWork.HasChanges()) return true; @@ -205,8 +204,11 @@ public class ReadingListService : IReadingListService { var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(dto.ReadingListId)).ToList(); var item = items.Find(r => r.Id == dto.ReadingListItemId); - items.Remove(item); - items.Insert(dto.ToPosition, item); + if (item != null) + { + items.Remove(item); + items.Insert(dto.ToPosition, item); + } for (var i = 0; i < items.Count; i++) { @@ -226,6 +228,7 @@ public class ReadingListService : IReadingListService public async Task DeleteReadingListItem(UpdateReadingListPosition dto) { 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(); var index = 0; @@ -260,7 +263,8 @@ public class ReadingListService : IReadingListService private async Task CalculateReadingListAgeRating(ReadingList readingList, IEnumerable seriesIds) { var ageRating = await _unitOfWork.SeriesRepository.GetMaxAgeRatingFromSeriesAsync(seriesIds); - readingList.AgeRating = ageRating; + if (ageRating == null) readingList.AgeRating = AgeRating.Unknown; + else readingList.AgeRating = (AgeRating) ageRating; } /// @@ -274,7 +278,7 @@ public class ReadingListService : IReadingListService // We need full reading list with items as this is used by many areas that manipulate items var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(username, AppUserIncludes.ReadingListsWithItems); - if (!await UserHasReadingListAccess(readingListId, user)) + if (user == null || !await UserHasReadingListAccess(readingListId, user)) { return null; } @@ -302,6 +306,7 @@ public class ReadingListService : IReadingListService public async Task DeleteReadingList(int readingListId, AppUser user) { var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListId); + if (readingList == null) return true; user.ReadingLists.Remove(readingList); if (!_unitOfWork.HasChanges()) return true; @@ -322,7 +327,7 @@ public class ReadingListService : IReadingListService var lastOrder = 0; if (readingList.Items.Any()) { - lastOrder = readingList.Items.DefaultIfEmpty().Max(rli => rli.Order); + lastOrder = readingList.Items.DefaultIfEmpty().Max(rli => rli!.Order); } var existingChapterExists = readingList.Items.Select(rli => rli.ChapterId).ToHashSet(); diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index 42cb77049..3399c2df0 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -8,14 +8,12 @@ using API.Data; using API.Data.Repositories; using API.DTOs; using API.DTOs.CollectionTags; -using API.DTOs.Metadata; using API.DTOs.SeriesDetail; using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; using API.Helpers; using API.SignalR; -using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace API.Services; @@ -52,7 +50,7 @@ public class SeriesService : ISeriesService /// /// /// - public static Chapter GetFirstChapterForMetadata(Series series, bool isBookLibrary) + public static Chapter? GetFirstChapterForMetadata(Series series, bool isBookLibrary) { return series.Volumes.OrderBy(v => v.Number, ChapterSortComparer.Default) .SelectMany(v => v.Chapters.OrderBy(c => float.Parse(c.Number), ChapterSortComparer.Default)) @@ -65,12 +63,13 @@ public class SeriesService : ISeriesService { var seriesId = updateSeriesMetadataDto.SeriesMetadata.SeriesId; var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); + if (series == null) return false; var allCollectionTags = (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()).ToList(); var allGenres = (await _unitOfWork.GenreRepository.GetAllGenresAsync()).ToList(); var allPeople = (await _unitOfWork.PersonRepository.GetAllPeople()).ToList(); var allTags = (await _unitOfWork.TagRepository.GetAllTagsAsync()).ToList(); - series.Metadata ??= DbFactory.SeriesMetadata(updateSeriesMetadataDto.CollectionTags + series.Metadata ??= DbFactory.SeriesMetadata((updateSeriesMetadataDto.CollectionTags ?? new List()) .Select(dto => DbFactory.CollectionTag(dto.Id, dto.Title, dto.Summary, dto.Promoted)).ToList()); if (series.Metadata.AgeRating != updateSeriesMetadataDto.SeriesMetadata.AgeRating) @@ -104,13 +103,13 @@ public class SeriesService : ISeriesService if (series.Metadata.Summary != updateSeriesMetadataDto.SeriesMetadata.Summary.Trim()) { - series.Metadata.Summary = updateSeriesMetadataDto.SeriesMetadata?.Summary.Trim(); + series.Metadata.Summary = updateSeriesMetadataDto.SeriesMetadata?.Summary.Trim() ?? string.Empty; series.Metadata.SummaryLocked = true; } if (series.Metadata.Language != updateSeriesMetadataDto.SeriesMetadata?.Language) { - series.Metadata.Language = updateSeriesMetadataDto.SeriesMetadata?.Language; + series.Metadata.Language = updateSeriesMetadataDto.SeriesMetadata?.Language ?? string.Empty; series.Metadata.LanguageLocked = true; } @@ -121,13 +120,13 @@ public class SeriesService : ISeriesService }); series.Metadata.Genres ??= new List(); - UpdateGenreList(updateSeriesMetadataDto.SeriesMetadata?.Genres, series, allGenres, (genre) => + GenreHelper.UpdateGenreList(updateSeriesMetadataDto.SeriesMetadata?.Genres, series, allGenres, (genre) => { series.Metadata.Genres.Add(genre); }, () => series.Metadata.GenresLocked = true); series.Metadata.Tags ??= new List(); - UpdateTagList(updateSeriesMetadataDto.SeriesMetadata.Tags, series, allTags, (tag) => + TagHelper.UpdateTagList(updateSeriesMetadataDto.SeriesMetadata?.Tags, series, allTags, (tag) => { series.Metadata.Tags.Add(tag); }, () => series.Metadata.TagsLocked = true); @@ -139,25 +138,25 @@ public class SeriesService : ISeriesService } series.Metadata.People ??= new List(); - UpdatePeopleList(PersonRole.Writer, updateSeriesMetadataDto.SeriesMetadata.Writers, series, allPeople, + PersonHelper.UpdatePeopleList(PersonRole.Writer, updateSeriesMetadataDto.SeriesMetadata!.Writers, series, allPeople, HandleAddPerson, () => series.Metadata.WriterLocked = true); - UpdatePeopleList(PersonRole.Character, updateSeriesMetadataDto.SeriesMetadata.Characters, series, allPeople, + PersonHelper.UpdatePeopleList(PersonRole.Character, updateSeriesMetadataDto.SeriesMetadata.Characters, series, allPeople, HandleAddPerson, () => series.Metadata.CharacterLocked = true); - UpdatePeopleList(PersonRole.Colorist, updateSeriesMetadataDto.SeriesMetadata.Colorists, series, allPeople, + PersonHelper.UpdatePeopleList(PersonRole.Colorist, updateSeriesMetadataDto.SeriesMetadata.Colorists, series, allPeople, HandleAddPerson, () => series.Metadata.ColoristLocked = true); - UpdatePeopleList(PersonRole.Editor, updateSeriesMetadataDto.SeriesMetadata.Editors, series, allPeople, + PersonHelper.UpdatePeopleList(PersonRole.Editor, updateSeriesMetadataDto.SeriesMetadata.Editors, series, allPeople, HandleAddPerson, () => series.Metadata.EditorLocked = true); - UpdatePeopleList(PersonRole.Inker, updateSeriesMetadataDto.SeriesMetadata.Inkers, series, allPeople, + PersonHelper.UpdatePeopleList(PersonRole.Inker, updateSeriesMetadataDto.SeriesMetadata.Inkers, series, allPeople, HandleAddPerson, () => series.Metadata.InkerLocked = true); - UpdatePeopleList(PersonRole.Letterer, updateSeriesMetadataDto.SeriesMetadata.Letterers, series, allPeople, + PersonHelper.UpdatePeopleList(PersonRole.Letterer, updateSeriesMetadataDto.SeriesMetadata.Letterers, series, allPeople, HandleAddPerson, () => series.Metadata.LettererLocked = true); - UpdatePeopleList(PersonRole.Penciller, updateSeriesMetadataDto.SeriesMetadata.Pencillers, series, allPeople, + PersonHelper.UpdatePeopleList(PersonRole.Penciller, updateSeriesMetadataDto.SeriesMetadata.Pencillers, series, allPeople, HandleAddPerson, () => series.Metadata.PencillerLocked = true); - UpdatePeopleList(PersonRole.Publisher, updateSeriesMetadataDto.SeriesMetadata.Publishers, series, allPeople, + PersonHelper.UpdatePeopleList(PersonRole.Publisher, updateSeriesMetadataDto.SeriesMetadata.Publishers, series, allPeople, HandleAddPerson, () => series.Metadata.PublisherLocked = true); - UpdatePeopleList(PersonRole.Translator, updateSeriesMetadataDto.SeriesMetadata.Translators, series, allPeople, + PersonHelper.UpdatePeopleList(PersonRole.Translator, updateSeriesMetadataDto.SeriesMetadata.Translators, series, allPeople, HandleAddPerson, () => series.Metadata.TranslatorLocked = true); - UpdatePeopleList(PersonRole.CoverArtist, updateSeriesMetadataDto.SeriesMetadata.CoverArtists, series, allPeople, + PersonHelper.UpdatePeopleList(PersonRole.CoverArtist, updateSeriesMetadataDto.SeriesMetadata.CoverArtists, series, allPeople, HandleAddPerson, () => series.Metadata.CoverArtistLocked = true); series.Metadata.AgeRatingLocked = updateSeriesMetadataDto.SeriesMetadata.AgeRatingLocked; @@ -183,7 +182,7 @@ public class SeriesService : ISeriesService return true; } - if (await _unitOfWork.CommitAsync()) + if (await _unitOfWork.CommitAsync() && updateSeriesMetadataDto.CollectionTags != null) { foreach (var tag in updateSeriesMetadataDto.CollectionTags) { @@ -210,10 +209,10 @@ public class SeriesService : ISeriesService } - public static void UpdateCollectionsList(ICollection tags, Series series, IReadOnlyCollection allTags, + public static void UpdateCollectionsList(ICollection? tags, Series series, IReadOnlyCollection allTags, Action handleAdd) { - // TODO: Move UpdateRelatedList to a helper so we can easily test + // TODO: Move UpdateCollectionsList to a helper so we can easily test if (tags == null) return; // I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different var existingTags = series.Metadata.CollectionTags.ToList(); @@ -245,142 +244,13 @@ public class SeriesService : ISeriesService } } - private static void UpdateGenreList(ICollection tags, Series series, IReadOnlyCollection allTags, Action handleAdd, Action onModified) - { - if (tags == null) return; - var isModified = false; - // I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different - var existingTags = series.Metadata.Genres.ToList(); - foreach (var existing in existingTags) - { - // NOTE: Why don't I use a NormalizedName here (outside of memory pressure from string creation)? - if (tags.SingleOrDefault(t => t.Id == existing.Id) == null) - { - // Remove tag - series.Metadata.Genres.Remove(existing); - isModified = true; - } - } - - // At this point, all tags that aren't in dto have been removed. - foreach (var tagTitle in tags.Select(t => t.Title)) - { - var normalizedTitle = Tasks.Scanner.Parser.Parser.Normalize(tagTitle); - var existingTag = allTags.SingleOrDefault(t => t.NormalizedTitle == normalizedTitle); - if (existingTag != null) - { - if (series.Metadata.Genres.All(t => t.NormalizedTitle != normalizedTitle)) - { - handleAdd(existingTag); - isModified = true; - } - } - else - { - // Add new tag - handleAdd(DbFactory.Genre(tagTitle)); - isModified = true; - } - } - - if (isModified) - { - onModified(); - } - } - - private static void UpdateTagList(ICollection tags, Series series, IReadOnlyCollection allTags, Action handleAdd, Action onModified) - { - if (tags == null) return; - - var isModified = false; - // I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different - var existingTags = series.Metadata.Tags.ToList(); - foreach (var existing in existingTags.Where(existing => tags.SingleOrDefault(t => t.Id == existing.Id) == null)) - { - // Remove tag - series.Metadata.Tags.Remove(existing); - isModified = true; - } - - // At this point, all tags that aren't in dto have been removed. - foreach (var tagTitle in tags.Select(t => t.Title)) - { - var normalizedTitle = Tasks.Scanner.Parser.Parser.Normalize(tagTitle); - var existingTag = allTags.SingleOrDefault(t => t.NormalizedTitle.Equals(normalizedTitle)); - if (existingTag != null) - { - if (series.Metadata.Tags.All(t => t.NormalizedTitle != normalizedTitle)) - { - - handleAdd(existingTag); - isModified = true; - } - } - else - { - // Add new tag - handleAdd(DbFactory.Tag(tagTitle)); - isModified = true; - } - } - - if (isModified) - { - onModified(); - } - } - - private static void UpdatePeopleList(PersonRole role, ICollection tags, Series series, IReadOnlyCollection allTags, - Action handleAdd, Action onModified) - { - if (tags == null) return; - var isModified = false; - // I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different - var existingTags = series.Metadata.People.Where(p => p.Role == role).ToList(); - foreach (var existing in existingTags) - { - if (tags.SingleOrDefault(t => t.Id == existing.Id) == null) // This needs to check against role - { - // Remove tag - series.Metadata.People.Remove(existing); - isModified = true; - } - } - - // At this point, all tags that aren't in dto have been removed. - foreach (var tag in tags) - { - var existingTag = allTags.SingleOrDefault(t => t.Name == tag.Name && t.Role == tag.Role); - if (existingTag != null) - { - if (series.Metadata.People.Where(t => t.Role == tag.Role).All(t => !t.Name.Equals(tag.Name))) - { - handleAdd(existingTag); - isModified = true; - } - } - else - { - // Add new tag - handleAdd(DbFactory.Person(tag.Name, role)); - isModified = true; - } - } - - if (isModified) - { - onModified(); - } - } - /// /// /// /// User with Ratings includes /// /// - public async Task UpdateRating(AppUser user, UpdateSeriesRatingDto updateSeriesRatingDto) + public async Task UpdateRating(AppUser? user, UpdateSeriesRatingDto updateSeriesRatingDto) { if (user == null) { @@ -478,10 +348,10 @@ public class SeriesService : ISeriesService throw new UnauthorizedAccessException("User does not have access to the library this series belongs to"); var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - if (user.AgeRestriction != AgeRating.NotApplicable) + if (user!.AgeRestriction != AgeRating.NotApplicable) { var seriesMetadata = await _unitOfWork.SeriesRepository.GetSeriesMetadata(seriesId); - if (seriesMetadata.AgeRating > user.AgeRestriction) + if (seriesMetadata!.AgeRating > user.AgeRestriction) throw new UnauthorizedAccessException("User is not allowed to view this series due to age restrictions"); } @@ -592,8 +462,10 @@ public class SeriesService : ISeriesService } - private static string FormatChapterTitle(bool isSpecial, LibraryType libraryType, string chapterTitle, bool withHash) + private static string FormatChapterTitle(bool isSpecial, LibraryType libraryType, string? chapterTitle, bool withHash) { + if (string.IsNullOrEmpty(chapterTitle)) throw new ArgumentException("Chapter Title cannot be null"); + if (isSpecial) { return Tasks.Scanner.Parser.Parser.CleanSpecialTitle(chapterTitle); @@ -649,6 +521,7 @@ public class SeriesService : ISeriesService public async Task UpdateRelatedSeries(UpdateRelatedSeriesDto dto) { var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, SeriesIncludes.Related); + if (series == null) return false; UpdateRelationForKind(dto.Adaptations, series.Relations.Where(r => r.RelationKind == RelationKind.Adaptation).ToList(), series, RelationKind.Adaptation); UpdateRelationForKind(dto.Characters, series.Relations.Where(r => r.RelationKind == RelationKind.Character).ToList(), series, RelationKind.Character); diff --git a/API/Services/StatisticService.cs b/API/Services/StatisticService.cs index 8b182e8c0..6b7a373b8 100644 --- a/API/Services/StatisticService.cs +++ b/API/Services/StatisticService.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading.Tasks; using API.Data; @@ -12,7 +11,6 @@ using API.Extensions; using AutoMapper; using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; namespace API.Services; @@ -593,13 +591,14 @@ public class StatisticService : IStatisticService user[userChapter.User.Id] = libraryTimes; } + return user.Keys.Select(userId => new TopReadDto() { UserId = userId, Username = users.First(u => u.Id == userId).UserName, - BooksTime = user[userId].ContainsKey(LibraryType.Book) ? user[userId][LibraryType.Book] : 0, - ComicsTime = user[userId].ContainsKey(LibraryType.Comic) ? user[userId][LibraryType.Comic] : 0, - MangaTime = user[userId].ContainsKey(LibraryType.Manga) ? user[userId][LibraryType.Manga] : 0, + BooksTime = user[userId].TryGetValue(LibraryType.Book, out var bookTime) ? bookTime : 0, + ComicsTime = user[userId].TryGetValue(LibraryType.Comic, out var comicTime) ? comicTime : 0, + MangaTime = user[userId].TryGetValue(LibraryType.Manga, out var mangaTime) ? mangaTime : 0, }) .ToList(); } diff --git a/API/Services/TachiyomiService.cs b/API/Services/TachiyomiService.cs index 23f57562d..13e2fae3a 100644 --- a/API/Services/TachiyomiService.cs +++ b/API/Services/TachiyomiService.cs @@ -15,7 +15,7 @@ namespace API.Services; public interface ITachiyomiService { - Task GetLatestChapter(int seriesId, int userId); + Task GetLatestChapter(int seriesId, int userId); Task MarkChaptersUntilAsRead(AppUser userWithProgress, int seriesId, float chapterNumber); } @@ -49,10 +49,8 @@ public class TachiyomiService : ITachiyomiService /// 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) + public async Task GetLatestChapter(int seriesId, int userId) { - - var currentChapter = await _readerService.GetContinuePoint(seriesId, userId); var prevChapterId = @@ -95,7 +93,8 @@ public class TachiyomiService : ITachiyomiService } // There is progress, we now need to figure out the highest volume or chapter and return that. - var prevChapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(prevChapterId); + var prevChapter = (await _unitOfWork.ChapterRepository.GetChapterDtoAsync(prevChapterId))!; + var volumeWithProgress = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(prevChapter.VolumeId, userId); // We only encode for single-file volumes if (volumeWithProgress.Number != 0 && volumeWithProgress.Chapters.Count == 1) diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index bdd7069bc..9a7dad1f5 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Threading; using System.Threading.Tasks; using API.Data; using API.Entities.Enums; @@ -322,6 +321,7 @@ public class TaskScheduler : ITaskScheduler public async Task CheckForUpdate() { var update = await _versionUpdaterService.CheckForUpdate(); + if (update == null) return; await _versionUpdaterService.PushUpdate(update); } diff --git a/API/Services/Tasks/BackupService.cs b/API/Services/Tasks/BackupService.cs index 134a82f90..6cc52ff9f 100644 --- a/API/Services/Tasks/BackupService.cs +++ b/API/Services/Tasks/BackupService.cs @@ -6,12 +6,9 @@ using System.Linq; using System.Threading.Tasks; using API.Data; using API.Entities.Enums; -using API.Extensions; using API.Logging; using API.SignalR; using Hangfire; -using Microsoft.AspNetCore.SignalR; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace API.Services.Tasks; @@ -62,7 +59,7 @@ public class BackupService : IBackupService public IEnumerable GetLogFiles(bool rollFiles = LogLevelOptions.LogRollingEnabled) { var multipleFileRegex = rollFiles ? @"\d*" : string.Empty; - var fi = _directoryService.FileSystem.FileInfo.FromFileName(LogLevelOptions.LogFile); + var fi = _directoryService.FileSystem.FileInfo.New(LogLevelOptions.LogFile); var files = rollFiles ? _directoryService.GetFiles(_directoryService.LogDirectory, diff --git a/API/Services/Tasks/CleanupService.cs b/API/Services/Tasks/CleanupService.cs index 0cc4d7c98..44d28065b 100644 --- a/API/Services/Tasks/CleanupService.cs +++ b/API/Services/Tasks/CleanupService.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading.Tasks; using API.Data; @@ -11,7 +10,6 @@ using API.Entities.Enums; using API.Helpers; using API.SignalR; using Hangfire; -using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; namespace API.Services.Tasks; @@ -175,7 +173,7 @@ public class CleanupService : ICleanupService var deltaTime = DateTime.Today.Subtract(TimeSpan.FromDays(dayThreshold)); var allBackups = _directoryService.GetFiles(backupDirectory).ToList(); - var expiredBackups = allBackups.Select(filename => _directoryService.FileSystem.FileInfo.FromFileName(filename)) + var expiredBackups = allBackups.Select(filename => _directoryService.FileSystem.FileInfo.New(filename)) .Where(f => f.CreationTime < deltaTime) .ToList(); @@ -198,7 +196,7 @@ public class CleanupService : ICleanupService 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.FromFileName(filename)) + var expiredLogs = allLogs.Select(filename => _directoryService.FileSystem.FileInfo.New(filename)) .Where(f => f.CreationTime < deltaTime) .ToList(); diff --git a/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs b/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs index 24075abf3..b4687d749 100644 --- a/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs +++ b/API/Services/Tasks/Metadata/WordCountAnalyzerService.cs @@ -3,7 +3,6 @@ using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using API.Data; -using API.Data.Repositories; using API.Entities; using API.Entities.Enums; using API.Helpers; @@ -50,7 +49,8 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService public async Task ScanLibrary(int libraryId, bool forceUpdate = false) { var sw = Stopwatch.StartNew(); - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.None); + var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId); + if (library == null) return; await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.WordCountAnalyzerProgressEvent(libraryId, 0F, ProgressEventType.Started, string.Empty)); diff --git a/API/Services/Tasks/Scanner/ParseScannedFiles.cs b/API/Services/Tasks/Scanner/ParseScannedFiles.cs index 230222cf7..bc44b81ff 100644 --- a/API/Services/Tasks/Scanner/ParseScannedFiles.cs +++ b/API/Services/Tasks/Scanner/ParseScannedFiles.cs @@ -18,24 +18,24 @@ public class ParsedSeries /// /// Name of the Series /// - public string Name { get; init; } + public required string Name { get; init; } /// /// Normalized Name of the Series /// - public string NormalizedName { get; init; } + public required string NormalizedName { get; init; } /// /// Format of the Series /// - public MangaFormat Format { get; init; } + public required MangaFormat Format { get; init; } } public class SeriesModified { - public string FolderPath { get; set; } - public string SeriesName { get; set; } + public required string FolderPath { get; set; } + public required string SeriesName { get; set; } public DateTime LastScanned { get; set; } public MangaFormat Format { get; set; } - public IEnumerable LibraryRoots { get; set; } + public IEnumerable LibraryRoots { get; set; } = ArraySegment.Empty; } /// @@ -166,16 +166,16 @@ public class ParseScannedFiles /// /// A localized list of a series' parsed infos /// - private void TrackSeries(ConcurrentDictionary> scannedSeries, ParserInfo info) + private void TrackSeries(ConcurrentDictionary> scannedSeries, ParserInfo? info) { - if (info.Series == string.Empty) return; + if (info == null || info.Series == string.Empty) return; // Check if normalized info.Series already exists and if so, update info to use that name instead info.Series = MergeName(scannedSeries, info); - var normalizedSeries = Parser.Parser.Normalize(info.Series); - var normalizedSortSeries = Parser.Parser.Normalize(info.SeriesSort); - var normalizedLocalizedSeries = Parser.Parser.Normalize(info.LocalizedSeries); + var normalizedSeries = info.Series.ToNormalized(); + var normalizedSortSeries = info.SeriesSort.ToNormalized(); + var normalizedLocalizedSeries = info.LocalizedSeries.ToNormalized(); try { @@ -224,19 +224,19 @@ public class ParseScannedFiles /// Series Name to group this info into private string MergeName(ConcurrentDictionary> scannedSeries, ParserInfo info) { - var normalizedSeries = Parser.Parser.Normalize(info.Series); - var normalizedLocalSeries = Parser.Parser.Normalize(info.LocalizedSeries); + var normalizedSeries = info.Series.ToNormalized(); + var normalizedLocalSeries = info.LocalizedSeries.ToNormalized(); try { var existingName = scannedSeries.SingleOrDefault(p => - (Parser.Parser.Normalize(p.Key.NormalizedName).Equals(normalizedSeries) || - Parser.Parser.Normalize(p.Key.NormalizedName).Equals(normalizedLocalSeries)) && + (p.Key.NormalizedName.ToNormalized().Equals(normalizedSeries) || + p.Key.NormalizedName.ToNormalized().Equals(normalizedLocalSeries)) && p.Key.Format == info.Format) .Key; - if (existingName != null && !string.IsNullOrEmpty(existingName.Name)) + if (!string.IsNullOrEmpty(existingName.Name)) { return existingName.Name; } @@ -245,8 +245,8 @@ public class ParseScannedFiles { _logger.LogCritical(ex, "[ScannerService] Multiple series detected for {SeriesName} ({File})! This is critical to fix! There should only be 1", info.Series, info.FullFilePath); var values = scannedSeries.Where(p => - (Parser.Parser.Normalize(p.Key.NormalizedName) == normalizedSeries || - Parser.Parser.Normalize(p.Key.NormalizedName) == normalizedLocalSeries) && + (p.Key.NormalizedName.ToNormalized() == normalizedSeries || + p.Key.NormalizedName.ToNormalized() == normalizedLocalSeries) && p.Key.Format == info.Format); foreach (var pair in values) { @@ -272,7 +272,7 @@ public class ParseScannedFiles /// public async Task ScanLibrariesForSeries(LibraryType libraryType, IEnumerable folders, string libraryName, bool isLibraryScan, - IDictionary> seriesPaths, Func>, Task> processSeriesInfos, bool forceCheck = false) + IDictionary> seriesPaths, Func>, Task>? processSeriesInfos, bool forceCheck = false) { await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.FileScanProgressEvent("File Scan Starting", libraryName, ProgressEventType.Started)); @@ -287,7 +287,8 @@ public class ParseScannedFiles Series = fp.SeriesName, Format = fp.Format, }).ToList(); - await processSeriesInfos.Invoke(new Tuple>(true, parsedInfos)); + if (processSeriesInfos != null) + await processSeriesInfos.Invoke(new Tuple>(true, parsedInfos)); _logger.LogDebug("[ScannerService] Skipped File Scan for {Folder} as it hasn't changed since last scan", folder); await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.FileScanProgressEvent("Skipped " + normalizedFolder, libraryName, ProgressEventType.Updated)); @@ -310,7 +311,7 @@ public class ParseScannedFiles .ToList(); - MergeLocalizedSeriesWithSeries(infos); + MergeLocalizedSeriesWithSeries(infos!); foreach (var info in infos) { @@ -322,7 +323,7 @@ public class ParseScannedFiles { _logger.LogError(ex, "[ScannerService] There was an exception that occurred during tracking {FilePath}. Skipping this file", - info.FullFilePath); + info?.FullFilePath); } } @@ -390,7 +391,7 @@ public class ParseScannedFiles if (string.IsNullOrEmpty(localizedSeries)) return; // NOTE: If we have multiple series in a folder with a localized title, then this will fail. It will group into one series. User needs to fix this themselves. - string nonLocalizedSeries; + string? nonLocalizedSeries; // Normalize this as many of the cases is a capitalization difference var nonLocalizedSeriesFound = infos .Where(i => !i.IsSpecial) @@ -409,11 +410,11 @@ public class ParseScannedFiles nonLocalizedSeries = nonLocalizedSeriesFound.FirstOrDefault(s => !s.Equals(localizedSeries)); } - if (string.IsNullOrEmpty(nonLocalizedSeries)) return; + if (nonLocalizedSeries == null) return; - var normalizedNonLocalizedSeries = Parser.Parser.Normalize(nonLocalizedSeries); + var normalizedNonLocalizedSeries = nonLocalizedSeries.ToNormalized(); foreach (var infoNeedingMapping in infos.Where(i => - !Parser.Parser.Normalize(i.Series).Equals(normalizedNonLocalizedSeries))) + !i.Series.ToNormalized().Equals(normalizedNonLocalizedSeries))) { infoNeedingMapping.Series = nonLocalizedSeries; infoNeedingMapping.LocalizedSeries = localizedSeries; diff --git a/API/Services/Tasks/Scanner/Parser/DefaultParser.cs b/API/Services/Tasks/Scanner/Parser/DefaultParser.cs index 072b1e44e..9b3053e9a 100644 --- a/API/Services/Tasks/Scanner/Parser/DefaultParser.cs +++ b/API/Services/Tasks/Scanner/Parser/DefaultParser.cs @@ -7,7 +7,7 @@ namespace API.Services.Tasks.Scanner.Parser; public interface IDefaultParser { - ParserInfo Parse(string filePath, string rootPath, LibraryType type = LibraryType.Manga); + ParserInfo? Parse(string filePath, string rootPath, LibraryType type = LibraryType.Manga); void ParseFromFallbackFolders(string filePath, string rootPath, LibraryType type, ref ParserInfo ret); } @@ -31,7 +31,7 @@ public class DefaultParser : IDefaultParser /// Root folder /// Defaults to Manga. Allows different Regex to be used for parsing. /// or null if Series was empty - public ParserInfo Parse(string filePath, string rootPath, LibraryType type = LibraryType.Manga) + public ParserInfo? Parse(string filePath, string rootPath, LibraryType type = LibraryType.Manga) { 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. @@ -134,7 +134,7 @@ public class DefaultParser : IDefaultParser if (fallbackFolders.Count == 0) { - var rootFolderName = _directoryService.FileSystem.DirectoryInfo.FromDirectoryName(rootPath).Name; + var rootFolderName = _directoryService.FileSystem.DirectoryInfo.New(rootPath).Name; var series = Parser.ParseSeries(rootFolderName); if (string.IsNullOrEmpty(series)) diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index 39952c1fa..ba5ad1855 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -11,11 +11,13 @@ public static class Parser { public const string DefaultChapter = "0"; public const string DefaultVolume = "0"; + private const int RegexTimeoutMs = 5000000; // 500 ms public static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(500); public const string ImageFileExtensions = @"^(\.png|\.jpeg|\.jpg|\.webp|\.gif)"; public const string ArchiveFileExtensions = @"\.cbz|\.zip|\.rar|\.cbr|\.tar.gz|\.7zip|\.7z|\.cb7|\.cbt"; private const string BookFileExtensions = @"\.epub|\.pdf"; + private const string XmlRegexExtensions = @"\.xml"; public const string MacOsMetadataFileStartsWith = @"._"; public const string SupportedExtensions = @@ -24,6 +26,37 @@ public static class Parser private const RegexOptions MatchOptions = RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant; + private static readonly ImmutableArray FormatTagSpecialKeywords = ImmutableArray.Create( + "Special", "Reference", "Director's Cut", "Box Set", "Box-Set", "Annual", "Anthology", "Epilogue", + "One Shot", "One-Shot", "Prologue", "TPB", "Trade Paper Back", "Omnibus", "Compendium", "Absolute", "Graphic Novel", + "GN", "FCBD"); + + private static readonly char[] LeadingZeroesTrimChars = new[] { '0' }; + + private static readonly char[] SpacesAndSeparators = { '\0', '\t', '\r', ' ', '-', ','}; + + + private const string Number = @"\d+(\.\d)?"; + private const string NumberRange = Number + @"(-" + Number + @")?"; + + /// + /// non greedy matching of a string where parenthesis are balanced + /// + public const string BalancedParen = @"(?:[^()]|(?\()|(?<-open>\)))*?(?(open)(?!))"; + /// + /// non greedy matching of a string where square brackets are balanced + /// + public const string BalancedBracket = @"(?:[^\[\]]|(?\[)|(?<-open>\]))*?(?(open)(?!))"; + /// + /// Matches [Complete], release tags like [kmts] but not [ Complete ] or [kmts ] + /// + private const string TagsInBrackets = $@"\[(?!\s){BalancedBracket}(? + /// Common regex patterns present in both Comics and Mangas + /// + private const string CommonSpecial = @"Specials?|One[- ]?Shot|Extra(?:\sChapter)?(?=\s)|Art Collection|Side Stories|Bonus"; + + /// /// Matches against font-family css syntax. Does not match if url import has data: starting, as that is binary data /// @@ -44,7 +77,6 @@ public static class Parser MatchOptions, RegexTimeout); - private const string XmlRegexExtensions = @"\.xml"; private static readonly Regex ImageRegex = new Regex(ImageFileExtensions, MatchOptions, RegexTimeout); private static readonly Regex ArchiveFileRegex = new Regex(ArchiveFileExtensions, @@ -67,14 +99,6 @@ public static class Parser private static readonly Regex SpecialTokenRegex = new Regex(@"SP\d+", MatchOptions, RegexTimeout); - private const string Number = @"\d+(\.\d)?"; - private const string NumberRange = Number + @"(-" + Number + @")?"; - - // Some generic reusage regex patterns: - // - non greedy matching of a string where parenthesis are balanced - public const string BalancedParen = @"(?:[^()]|(?\()|(?<-open>\)))*?(?(open)(?!))"; - // - non greedy matching of a string where square brackets are balanced - public const string BalancedBrack = @"(?:[^\[\]]|(?\[)|(?<-open>\]))*?(?(open)(?!))"; private static readonly Regex[] MangaVolumeRegex = new[] { @@ -86,7 +110,6 @@ public static class Parser new Regex( @"(?.*)(\b|_)(?!\[)(vol\.?)(?\d+(-\d+)?)(?!\])", MatchOptions, RegexTimeout), - // TODO: In .NET 7, update this to use raw literal strings and apply the NumberRange everywhere // Historys Strongest Disciple Kenichi_v11_c90-98.zip or Dance in the Vampire Bund v16-17 new Regex( @"(?.*)(\b|_)(?!\[)v(?" + NumberRange + @")(?!\])", @@ -576,18 +599,12 @@ public static class Parser MatchOptions, RegexTimeout ); - // Matches [Complete], release tags like [kmts] but not [ Complete ] or [kmts ] - private const string TagsInBrackets = $@"\[(?!\s){BalancedBrack}(? FormatTagSpecialKeywords = ImmutableArray.Create( - "Special", "Reference", "Director's Cut", "Box Set", "Box-Set", "Annual", "Anthology", "Epilogue", - "One Shot", "One-Shot", "Prologue", "TPB", "Trade Paper Back", "Omnibus", "Compendium", "Absolute", "Graphic Novel", - "GN", "FCBD"); - private static readonly char[] LeadingZeroesTrimChars = new[] { '0' }; - - private static readonly char[] SpacesAndSeparators = { '\0', '\t', '\r', ' ', '-', ','}; public static MangaFormat ParseFormat(string filePath) { @@ -669,11 +680,10 @@ public static class Parser foreach (var regex in MangaSeriesRegex) { var matches = regex.Matches(filename); - foreach (var group in matches.Select(match => match.Groups["Series"]) - .Where(group => group.Success && group != Match.Empty)) - { - return CleanTitle(group.Value); - } + var group = matches + .Select(match => match.Groups["Series"]) + .FirstOrDefault(group => group.Success && group != Match.Empty); + if (group != null) return CleanTitle(group.Value); } return string.Empty; @@ -683,11 +693,10 @@ public static class Parser foreach (var regex in ComicSeriesRegex) { var matches = regex.Matches(filename); - foreach (var group in matches.Select(match => match.Groups["Series"]) - .Where(group => group.Success && group != Match.Empty)) - { - return CleanTitle(group.Value, true); - } + var group = matches + .Select(match => match.Groups["Series"]) + .FirstOrDefault(group => group.Success && group != Match.Empty); + if (group != null) return CleanTitle(group.Value, true); } return string.Empty; @@ -1028,9 +1037,9 @@ public static class Parser /// /manga/1\1 -> /manga/1/1 /// /// - public static string NormalizePath(string path) + public static string NormalizePath(string? path) { - return path.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + return string.IsNullOrEmpty(path) ? string.Empty : path.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) .Replace(@"//", Path.AltDirectorySeparatorChar + string.Empty); } @@ -1044,5 +1053,8 @@ public static class Parser return FormatTagSpecialKeywords.Contains(comicInfoFormat); } - private static string ReplaceUnderscores(string name) => name?.Replace("_", " "); + private static string ReplaceUnderscores(string name) + { + return string.IsNullOrEmpty(name) ? string.Empty : name.Replace('_', ' '); + } } diff --git a/API/Services/Tasks/Scanner/Parser/ParserInfo.cs b/API/Services/Tasks/Scanner/Parser/ParserInfo.cs index 1f0a9d692..ea5f1243e 100644 --- a/API/Services/Tasks/Scanner/Parser/ParserInfo.cs +++ b/API/Services/Tasks/Scanner/Parser/ParserInfo.cs @@ -17,7 +17,7 @@ public class ParserInfo /// /// Represents the parsed series from the file or folder /// - public string Series { get; set; } = string.Empty; + 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 /// @@ -80,14 +80,14 @@ public class ParserInfo /// 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; } + 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) + public void Merge(ParserInfo? info2) { if (info2 == null) return; Chapters = string.IsNullOrEmpty(Chapters) || Chapters == "0" ? info2.Chapters: Chapters; diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/API/Services/Tasks/Scanner/ProcessSeries.cs index 10a9ed313..6b01f3f6e 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/API/Services/Tasks/Scanner/ProcessSeries.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; -using System.Threading; using System.Threading.Tasks; using API.Data; using API.Data.Metadata; @@ -68,6 +67,11 @@ public class ProcessSeries : IProcessSeries _metadataService = metadataService; _wordCountAnalyzerService = wordCountAnalyzerService; _collectionTagService = collectionTagService; + + _genres = new Dictionary(); + _people = new List(); + _tags = new Dictionary(); + _collectionTags = new Dictionary(); } /// @@ -96,7 +100,7 @@ public class ProcessSeries : IProcessSeries // Check if there is a Series var firstInfo = parsedInfos.First(); - Series series; + Series? series; try { series = @@ -131,7 +135,7 @@ public class ProcessSeries : IProcessSeries UpdateVolumes(series, parsedInfos, forceUpdate); series.Pages = series.Volumes.Sum(v => v.Pages); - series.NormalizedName = Parser.Parser.Normalize(series.Name); + series.NormalizedName = series.Name.ToNormalized(); series.OriginalName ??= firstParsedInfo.Series; if (series.Format == MangaFormat.Unknown) { @@ -156,7 +160,7 @@ public class ProcessSeries : IProcessSeries if (!series.LocalizedNameLocked && !string.IsNullOrEmpty(localizedSeries)) { series.LocalizedName = localizedSeries; - series.NormalizedLocalizedName = Parser.Parser.Normalize(series.LocalizedName); + series.NormalizedLocalizedName = series.LocalizedName.ToNormalized(); } UpdateSeriesMetadata(series, library); @@ -299,17 +303,17 @@ public class ProcessSeries : IProcessSeries } } - if (!string.IsNullOrEmpty(firstChapter.Summary) && !series.Metadata.SummaryLocked) + if (!string.IsNullOrEmpty(firstChapter?.Summary) && !series.Metadata.SummaryLocked) { series.Metadata.Summary = firstChapter.Summary; } - if (!string.IsNullOrEmpty(firstChapter.Language) && !series.Metadata.LanguageLocked) + if (!string.IsNullOrEmpty(firstChapter?.Language) && !series.Metadata.LanguageLocked) { series.Metadata.Language = firstChapter.Language; } - if (!string.IsNullOrEmpty(firstChapter.SeriesGroup) && library.ManageCollections) + if (!string.IsNullOrEmpty(firstChapter?.SeriesGroup) && library.ManageCollections) { _logger.LogDebug("Collection tag(s) found for {SeriesName}, updating collections", series.Name); @@ -487,7 +491,7 @@ public class ProcessSeries : IProcessSeries foreach (var volumeNumber in distinctVolumes) { _logger.LogDebug("[ScannerService] Looking up volume for {VolumeNumber}", volumeNumber); - Volume volume; + Volume? volume; try { volume = series.Volumes.SingleOrDefault(s => s.Name == volumeNumber); @@ -568,7 +572,7 @@ public class ProcessSeries : IProcessSeries { // Specials go into their own chapters with Range being their filename and IsSpecial = True. Non-Specials with Vol and Chap as 0 // also are treated like specials for UI grouping. - Chapter chapter; + Chapter? chapter; try { chapter = volume.Chapters.GetChapterByRange(info); @@ -625,7 +629,7 @@ public class ProcessSeries : IProcessSeries { chapter.Files ??= new List(); var existingFile = chapter.Files.SingleOrDefault(f => f.FilePath == info.FullFilePath); - var fileInfo = _directoryService.FileSystem.FileInfo.FromFileName(info.FullFilePath); + var fileInfo = _directoryService.FileSystem.FileInfo.New(info.FullFilePath); if (existingFile != null) { existingFile.Format = info.Format; @@ -645,7 +649,6 @@ public class ProcessSeries : IProcessSeries } } - #nullable enable private void UpdateChapterFromComicInfo(Chapter chapter, ComicInfo? info) { var firstFile = chapter.Files.MinBy(x => x.Chapter); @@ -813,7 +816,6 @@ public class ProcessSeries : IProcessSeries } return ImmutableList.Empty; } - #nullable disable /// /// Given a list of all existing people, this will check the new names and roles and if it doesn't exist in allPeople, will create and @@ -830,9 +832,9 @@ public class ProcessSeries : IProcessSeries foreach (var name in names) { - var normalizedName = Parser.Parser.Normalize(name); + var normalizedName = name.ToNormalized(); var person = allPeopleTypeRole.FirstOrDefault(p => - p.NormalizedName.Equals(normalizedName)); + p.NormalizedName != null && p.NormalizedName.Equals(normalizedName)); if (person == null) { person = DbFactory.Person(name, role); @@ -855,7 +857,7 @@ public class ProcessSeries : IProcessSeries { foreach (var name in names) { - var normalizedName = Parser.Parser.Normalize(name); + var normalizedName = name.ToNormalized(); if (string.IsNullOrEmpty(normalizedName)) continue; _genres.TryGetValue(normalizedName, out var genre); @@ -885,7 +887,7 @@ public class ProcessSeries : IProcessSeries { if (string.IsNullOrEmpty(name.Trim())) continue; - var normalizedName = Parser.Parser.Normalize(name); + var normalizedName = name.ToNormalized(); _tags.TryGetValue(normalizedName, out var tag); var added = tag == null; diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index 26962183d..20c65e92f 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -9,6 +9,7 @@ using API.Data; using API.Data.Repositories; using API.Entities; using API.Entities.Enums; +using API.Extensions; using API.Helpers; using API.Parser; using API.Services.Tasks.Metadata; @@ -115,7 +116,7 @@ public class ScannerService : IScannerService foreach (var file in missingExtensions) { - var fileInfo = _directoryService.FileSystem.FileInfo.FromFileName(file.FilePath); + var fileInfo = _directoryService.FileSystem.FileInfo.New(file.FilePath); if (!fileInfo.Exists)continue; file.Extension = fileInfo.Extension.ToLowerInvariant(); file.Bytes = fileInfo.Length; @@ -134,7 +135,7 @@ public class ScannerService : IScannerService /// public async Task ScanFolder(string folder) { - Series series = null; + Series? series = null; try { series = await _unitOfWork.SeriesRepository.GetSeriesByFolderPath(folder, SeriesIncludes.Library); @@ -193,6 +194,7 @@ public class ScannerService : IScannerService if (series == null) return; // This can occur when UI deletes a series but doesn't update and user re-requests update var chapterIds = await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new[] {seriesId}); var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId, LibraryIncludes.Folders); + if (library == null) return; var libraryPaths = library.Folders.Select(f => f.Path).ToList(); if (await ShouldScanSeries(seriesId, library, libraryPaths, series, true) != ScanCancelReason.NoCancel) { @@ -216,7 +218,7 @@ public class ScannerService : IScannerService folderPath = seriesDirs.Keys.FirstOrDefault(); // We should check if folderPath is a library folder path and if so, return early and tell user to correct their setup. - if (libraryPaths.Contains(folderPath)) + 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.")); @@ -246,12 +248,12 @@ public class ScannerService : IScannerService var foundParsedSeries = new ParsedSeries() { Name = parsedFiles.First().Series, - NormalizedName = Scanner.Parser.Parser.Normalize(parsedFiles.First().Series), + NormalizedName = parsedFiles.First().Series.ToNormalized(), Format = parsedFiles.First().Format }; // For Scan Series, we need to filter out anything that isn't our Series - if (!foundParsedSeries.NormalizedName.Equals(series.NormalizedName) && !foundParsedSeries.NormalizedName.Equals(Scanner.Parser.Parser.Normalize(series.OriginalName))) + if (!foundParsedSeries.NormalizedName.Equals(series.NormalizedName) && !foundParsedSeries.NormalizedName.Equals(series.OriginalName?.ToNormalized())) { return; } @@ -275,9 +277,7 @@ public class ScannerService : IScannerService if (parsedSeries.Count == 0) { var seriesFiles = (await _unitOfWork.SeriesRepository.GetFilesForSeries(series.Id)); - var anyFilesExist = seriesFiles.Where(f => f.FilePath.Contains(series.FolderPath)).Any(m => File.Exists(m.FilePath)); - - if (!anyFilesExist) + if (!string.IsNullOrEmpty(series.FolderPath) && !seriesFiles.Where(f => f.FilePath.Contains(series.FolderPath)).Any(m => File.Exists(m.FilePath))) { try { @@ -320,7 +320,8 @@ 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.FromFileName(f.FilePath).Directory.FullName) + .Select(f => _directoryService.FileSystem.FileInfo.New(f.FilePath).Directory?.FullName ?? string.Empty) + .Where(f => !string.IsNullOrEmpty(f)) .Distinct() .ToList(); @@ -463,7 +464,7 @@ public class ScannerService : IScannerService { var sw = Stopwatch.StartNew(); var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.Folders); - var libraryFolderPaths = library.Folders.Select(fp => fp.Path).ToList(); + var libraryFolderPaths = library!.Folders.Select(fp => fp.Path).ToList(); if (!await CheckMounts(library.Name, libraryFolderPaths)) return; @@ -589,7 +590,7 @@ public class ScannerService : IScannerService } private async Task ScanFiles(Library library, IEnumerable dirs, - bool isLibraryScan, Func>, Task> processSeriesInfos = null, bool forceChecks = false) + bool isLibraryScan, Func>, Task>? processSeriesInfos = null, bool forceChecks = false) { var scanner = new ParseScannedFiles(_logger, _directoryService, _readingItemService, _eventHub); var scanWatch = Stopwatch.StartNew(); @@ -602,26 +603,6 @@ public class ScannerService : IScannerService return scanElapsedTime; } - /// - /// Remove any user progress rows that no longer exist since scan library ran and deleted series/volumes/chapters - /// - private async Task CleanupAbandonedChapters() - { - var cleanedUp = await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters(); - _logger.LogInformation("Removed {Count} abandoned progress rows", cleanedUp); - } - - - /// - /// Cleans up any abandoned rows due to removals from Scan loop - /// - private async Task CleanupDbEntities() - { - await CleanupAbandonedChapters(); - var cleanedUp = await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries(); - _logger.LogInformation("Removed {Count} abandoned collection tags", cleanedUp); - } - public static IEnumerable FindSeriesNotOnDisk(IEnumerable existingSeries, Dictionary> parsedSeries) { return existingSeries.Where(es => !ParserInfoHelpers.SeriesHasMatchingParserInfoFormat(es, parsedSeries)); diff --git a/API/Services/Tasks/SiteThemeService.cs b/API/Services/Tasks/SiteThemeService.cs index 8de7f879c..da08b327b 100644 --- a/API/Services/Tasks/SiteThemeService.cs +++ b/API/Services/Tasks/SiteThemeService.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using API.Data; using API.Entities; using API.Entities.Enums.Theme; +using API.Extensions; using API.SignalR; using Kavita.Common; using Microsoft.AspNetCore.Authorization; @@ -56,7 +57,7 @@ public class ThemeService : IThemeService var reservedNames = Seed.DefaultThemes.Select(t => t.NormalizedName).ToList(); var themeFiles = _directoryService .GetFilesWithExtension(Scanner.Parser.Parser.NormalizePath(_directoryService.SiteThemeDirectory), @"\.css") - .Where(name => !reservedNames.Contains(Scanner.Parser.Parser.Normalize(name))).ToList(); + .Where(name => !reservedNames.Contains(name.ToNormalized())).ToList(); var allThemes = (await _unitOfWork.SiteThemeRepository.GetThemes()).ToList(); @@ -78,7 +79,7 @@ public class ThemeService : IThemeService foreach (var themeFile in themeFiles) { var themeName = - Scanner.Parser.Parser.Normalize(_directoryService.FileSystem.Path.GetFileNameWithoutExtension(themeFile)); + _directoryService.FileSystem.Path.GetFileNameWithoutExtension(themeFile).ToNormalized(); if (allThemeNames.Contains(themeName)) continue; _unitOfWork.SiteThemeRepository.Add(new SiteTheme() diff --git a/API/Services/Tasks/StatsService.cs b/API/Services/Tasks/StatsService.cs index a4cb355c5..e6035d23a 100644 --- a/API/Services/Tasks/StatsService.cs +++ b/API/Services/Tasks/StatsService.cs @@ -161,11 +161,11 @@ public class StatsService : IStatsService if (firstAdminUser != null) { - var firstAdminUserPref = (await _unitOfWork.UserRepository.GetPreferencesAsync(firstAdminUser.UserName)); - var activeTheme = firstAdminUserPref.Theme ?? Seed.DefaultThemes.First(t => t.IsDefault); + var firstAdminUserPref = (await _unitOfWork.UserRepository.GetPreferencesAsync(firstAdminUser.UserName!)); + var activeTheme = firstAdminUserPref?.Theme ?? Seed.DefaultThemes.First(t => t.IsDefault); serverInfo.ActiveSiteTheme = activeTheme.Name; - serverInfo.MangaReaderMode = firstAdminUserPref.ReaderMode; + if (firstAdminUserPref != null) serverInfo.MangaReaderMode = firstAdminUserPref.ReaderMode; } return serverInfo; @@ -242,7 +242,7 @@ public class StatsService : IStatsService // If first time flow, just return 0 if (!await _context.Series.AnyAsync()) return 0; return await _context.Series - .Select(s => _context.Library.Where(l => l.Id == s.LibraryId).SelectMany(l => l.Series).Count()) + .Select(s => _context.Library.Where(l => l.Id == s.LibraryId).SelectMany(l => l.Series!).Count()) .MaxAsync(); } @@ -254,7 +254,7 @@ public class StatsService : IStatsService .Select(v => new { v.SeriesId, - Count = _context.Series.Where(s => s.Id == v.SeriesId).SelectMany(s => s.Volumes).Count() + Count = _context.Series.Where(s => s.Id == v.SeriesId).SelectMany(s => s.Volumes!).Count() }) .AsNoTracking() .AsSplitQuery() @@ -268,9 +268,9 @@ public class StatsService : IStatsService return await _context.Series .AsNoTracking() .AsSplitQuery() - .MaxAsync(s => s.Volumes + .MaxAsync(s => s.Volumes! .Where(v => v.Number == 0) - .SelectMany(v => v.Chapters) + .SelectMany(v => v.Chapters!) .Count()); } @@ -292,13 +292,14 @@ public class StatsService : IStatsService private IEnumerable AllFormats() { + // TODO: Rewrite this with new migration code in feature/basic-stats var results = _context.MangaFile .AsNoTracking() .AsEnumerable() .Select(m => new FileFormatDto() { Format = m.Format, - Extension = Path.GetExtension(m.FilePath)?.ToLowerInvariant() + Extension = Path.GetExtension(m.FilePath)?.ToLowerInvariant()! }) .DistinctBy(f => f.Extension) .ToList(); diff --git a/API/Services/Tasks/VersionUpdaterService.cs b/API/Services/Tasks/VersionUpdaterService.cs index 8d79b3a45..a8e8b8ba4 100644 --- a/API/Services/Tasks/VersionUpdaterService.cs +++ b/API/Services/Tasks/VersionUpdaterService.cs @@ -4,48 +4,46 @@ using System.Linq; using System.Threading.Tasks; using API.DTOs.Update; using API.SignalR; -using API.SignalR.Presence; using Flurl.Http; using Kavita.Common.EnvironmentInfo; using Kavita.Common.Helpers; using MarkdownDeep; -using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace API.Services.Tasks; -internal class GithubReleaseMetadata +internal abstract class GithubReleaseMetadata { /// /// Name of the Tag /// v0.4.3 /// // ReSharper disable once InconsistentNaming - public string Tag_Name { get; init; } + public required string Tag_Name { get; init; } /// /// Name of the Release /// - public string Name { get; init; } + public required string Name { get; init; } /// /// Body of the Release /// - public string Body { get; init; } + public required string Body { get; init; } /// /// Url of the release on Github /// // ReSharper disable once InconsistentNaming - public string Html_Url { get; init; } + public required string Html_Url { get; init; } /// /// Date Release was Published /// // ReSharper disable once InconsistentNaming - public string Published_At { get; init; } + public required string Published_At { get; init; } } public interface IVersionUpdaterService { - Task CheckForUpdate(); + Task CheckForUpdate(); Task PushUpdate(UpdateNotificationDto update); Task> GetAllReleases(); } @@ -79,16 +77,17 @@ public class VersionUpdaterService : IVersionUpdaterService { var update = await GetGithubRelease(); var dto = CreateDto(update); + if (dto == null) return null; return new Version(dto.UpdateVersion) <= new Version(dto.CurrentVersion) ? null : dto; } public async Task> GetAllReleases() { var updates = await GetGithubReleases(); - return updates.Select(CreateDto); + return updates.Select(CreateDto).Where(d => d != null)!; } - private UpdateNotificationDto CreateDto(GithubReleaseMetadata update) + private UpdateNotificationDto? CreateDto(GithubReleaseMetadata? update) { if (update == null || string.IsNullOrEmpty(update.Tag_Name)) return null; var updateVersion = new Version(update.Tag_Name.Replace("v", string.Empty)); @@ -106,7 +105,7 @@ public class VersionUpdaterService : IVersionUpdaterService }; } - public async Task PushUpdate(UpdateNotificationDto update) + public async Task PushUpdate(UpdateNotificationDto? update) { if (update == null) return; diff --git a/API/Services/TokenService.cs b/API/Services/TokenService.cs index 923c3b1d7..1d28b6453 100644 --- a/API/Services/TokenService.cs +++ b/API/Services/TokenService.cs @@ -19,7 +19,7 @@ namespace API.Services; public interface ITokenService { Task CreateToken(AppUser user); - Task ValidateRefreshToken(TokenRequestDto request); + Task ValidateRefreshToken(TokenRequestDto request); Task CreateRefreshToken(AppUser user); } @@ -33,14 +33,14 @@ public class TokenService : ITokenService { _userManager = userManager; - _key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"])); + _key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"] ?? string.Empty)); } public async Task CreateToken(AppUser user) { var claims = new List { - new Claim(JwtRegisteredClaimNames.Name, user.UserName), + new Claim(JwtRegisteredClaimNames.Name, user.UserName!), new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), }; @@ -71,11 +71,12 @@ public class TokenService : ITokenService return refreshToken; } - public async Task ValidateRefreshToken(TokenRequestDto request) + public async Task ValidateRefreshToken(TokenRequestDto request) { var tokenHandler = new JwtSecurityTokenHandler(); var tokenContent = tokenHandler.ReadJwtToken(request.Token); var username = tokenContent.Claims.FirstOrDefault(q => q.Type == JwtRegisteredClaimNames.NameId)?.Value; + if (string.IsNullOrEmpty(username)) return null; var user = await _userManager.FindByNameAsync(username); if (user == null) return null; // This forces a logout await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, "RefreshToken", request.RefreshToken); diff --git a/API/SignalR/EventHub.cs b/API/SignalR/EventHub.cs index d5820ab09..3f5eed44a 100644 --- a/API/SignalR/EventHub.cs +++ b/API/SignalR/EventHub.cs @@ -1,4 +1,5 @@ using System.Linq; +using System; using System.Threading.Tasks; using API.Data; using API.SignalR.Presence; @@ -54,8 +55,8 @@ public class EventHub : IEventHub /// public async Task SendMessageToAsync(string method, SignalRMessage message, int userId) { - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - await _messageHub.Clients.User(user.UserName).SendAsync(method, message); + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId) ?? throw new InvalidOperationException(); + await _messageHub.Clients.User(user.UserName!).SendAsync(method, message); } } diff --git a/API/SignalR/LogHub.cs b/API/SignalR/LogHub.cs index 5158a389f..975711dfd 100644 --- a/API/SignalR/LogHub.cs +++ b/API/SignalR/LogHub.cs @@ -26,13 +26,13 @@ public class LogHub : Hub public override async Task OnConnectedAsync() { - await _tracker.UserConnected(Context.User.GetUserId(), Context.ConnectionId); + await _tracker.UserConnected(Context.User!.GetUserId(), Context.ConnectionId); await base.OnConnectedAsync(); } - public override async Task OnDisconnectedAsync(Exception exception) + public override async Task OnDisconnectedAsync(Exception? exception) { - await _tracker.UserDisconnected(Context.User.GetUserId(), Context.ConnectionId); + await _tracker.UserDisconnected(Context.User!.GetUserId(), Context.ConnectionId); await base.OnDisconnectedAsync(exception); } diff --git a/API/SignalR/MessageFactory.cs b/API/SignalR/MessageFactory.cs index 11c1e625b..e71d8fda8 100644 --- a/API/SignalR/MessageFactory.cs +++ b/API/SignalR/MessageFactory.cs @@ -1,9 +1,5 @@ using System; -using System.Diagnostics; -using System.IO; -using System.Threading; using API.DTOs.Update; -using API.Entities; using API.Extensions; namespace API.SignalR; diff --git a/API/SignalR/MessageHub.cs b/API/SignalR/MessageHub.cs index 23ceaa8be..1e75e13de 100644 --- a/API/SignalR/MessageHub.cs +++ b/API/SignalR/MessageHub.cs @@ -22,7 +22,7 @@ public class MessageHub : Hub public override async Task OnConnectedAsync() { - await _tracker.UserConnected(Context.User.GetUserId(), Context.ConnectionId); + await _tracker.UserConnected(Context.User!.GetUserId(), Context.ConnectionId); var currentUsers = await PresenceTracker.GetOnlineUsers(); await Clients.All.SendAsync(MessageFactory.OnlineUsers, currentUsers); @@ -31,9 +31,9 @@ public class MessageHub : Hub await base.OnConnectedAsync(); } - public override async Task OnDisconnectedAsync(Exception exception) + public override async Task OnDisconnectedAsync(Exception? exception) { - await _tracker.UserDisconnected(Context.User.GetUserId(), Context.ConnectionId); + await _tracker.UserDisconnected(Context.User!.GetUserId(), Context.ConnectionId); var currentUsers = await PresenceTracker.GetOnlineUsers(); await Clients.All.SendAsync(MessageFactory.OnlineUsers, currentUsers); diff --git a/API/SignalR/Presence/PresenceTracker.cs b/API/SignalR/Presence/PresenceTracker.cs index 1e9f930f3..45e2a0bcc 100644 --- a/API/SignalR/Presence/PresenceTracker.cs +++ b/API/SignalR/Presence/PresenceTracker.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.Data; @@ -18,7 +17,7 @@ public interface IPresenceTracker internal class ConnectionDetail { public string UserName { get; set; } - public List ConnectionIds { get; set; } + public List ConnectionIds { get; set; } = new List(); public bool IsAdmin { get; set; } } @@ -43,9 +42,9 @@ public class PresenceTracker : IPresenceTracker var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user); lock (OnlineUsers) { - if (OnlineUsers.ContainsKey(userId)) + if (OnlineUsers.TryGetValue(userId, out var detail)) { - OnlineUsers[userId].ConnectionIds.Add(connectionId); + detail.ConnectionIds.Add(connectionId); } else { @@ -104,7 +103,7 @@ public class PresenceTracker : IPresenceTracker public Task> GetConnectionsForUser(int userId) { - List connectionIds; + List? connectionIds; lock (OnlineUsers) { connectionIds = OnlineUsers.GetValueOrDefault(userId)?.ConnectionIds; diff --git a/API/SignalR/SignalRMessage.cs b/API/SignalR/SignalRMessage.cs index 6c8afe844..d3f250293 100644 --- a/API/SignalR/SignalRMessage.cs +++ b/API/SignalR/SignalRMessage.cs @@ -10,8 +10,8 @@ public class SignalRMessage /// /// Body of the event type /// - public object Body { get; set; } - public string Name { get; set; } + public object? Body { get; set; } + public required string Name { get; set; } /// /// User friendly Title of the Event /// diff --git a/API/Startup.cs b/API/Startup.cs index f84ef6387..60fd490ee 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -19,8 +19,6 @@ using API.Services.HostedServices; using API.Services.Tasks; using API.SignalR; using Hangfire; -using Hangfire.MemoryStorage; -using Hangfire.Storage.SQLite; using Kavita.Common; using Kavita.Common.EnvironmentInfo; using Microsoft.AspNetCore.Builder; @@ -111,7 +109,7 @@ public class Startup { options.ForwardedHeaders = ForwardedHeaders.All; foreach(var proxy in _config.GetSection("KnownProxies").AsEnumerable().Where(c => c.Value != null)) { - options.KnownProxies.Add(IPAddress.Parse(proxy.Value)); + options.KnownProxies.Add(IPAddress.Parse(proxy.Value!)); } }); services.AddCors(); diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index 060628183..700cb9775 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -1,7 +1,7 @@ - net6.0 + net7.0 kavitareader.com Kavita 0.7.1.5 @@ -12,13 +12,13 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - \ No newline at end of file + diff --git a/build.sh b/build.sh index 9721e9585..ef8ad3caf 100755 --- a/build.sh +++ b/build.sh @@ -75,17 +75,16 @@ BuildUI() Package() { - local framework="$1" - local runtime="$2" + local runtime="$1" local lOutputFolder=../_output/"$runtime"/Kavita - ProgressStart "Creating $runtime Package for $framework" + ProgressStart "Creating $runtime Package" # TODO: Use no-restore? Because Build should have already done it for us echo "Building" cd API - echo dotnet publish -c Release --self-contained --runtime $runtime -o "$lOutputFolder" --framework $framework - dotnet publish -c Release --self-contained --runtime $runtime -o "$lOutputFolder" --framework $framework + echo dotnet publish -c Release --self-contained --runtime $runtime -o "$lOutputFolder" + dotnet publish -c Release --self-contained --runtime $runtime -o "$lOutputFolder" echo "Recopying wwwroot due to bug" cp -R ./wwwroot/* $lOutputFolder/wwwroot @@ -114,7 +113,7 @@ Package() tar -czvf ../kavita-$runtime.tar.gz Kavita - ProgressEnd "Creating $runtime Package for $framework" + ProgressEnd "Creating $runtime Package" } @@ -130,21 +129,21 @@ dir=$PWD if [[ -z "$RID" ]]; then - Package "net6.0" "win-x64" + Package "win-x64" cd "$dir" - Package "net6.0" "win-x86" + Package "win-x86" cd "$dir" - Package "net6.0" "linux-x64" + Package "linux-x64" cd "$dir" - Package "net6.0" "linux-arm" + Package "linux-arm" cd "$dir" - Package "net6.0" "linux-arm64" + Package "linux-arm64" cd "$dir" - Package "net6.0" "linux-musl-x64" + Package "linux-musl-x64" cd "$dir" - Package "net6.0" "osx-x64" + Package "osx-x64" cd "$dir" else - Package "net6.0" "$RID" + Package "$RID" cd "$dir" fi diff --git a/global.json b/global.json index 97dd87392..36e1a9e95 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "6.0", + "version": "7.0.0", "rollForward": "latestMajor", "allowPrerelease": false } diff --git a/monorepo-build.sh b/monorepo-build.sh index d4c4b9b7d..f2190b397 100755 --- a/monorepo-build.sh +++ b/monorepo-build.sh @@ -30,17 +30,16 @@ Build() Package() { - local framework="$1" - local runtime="$2" + local runtime="$1" local lOutputFolder=../_output/"$runtime"/Kavita - ProgressStart "Creating $runtime Package for $framework" + ProgressStart "Creating $runtime Package" # TODO: Use no-restore? Because Build should have already done it for us echo "Building" cd API - echo dotnet publish -c Release --no-restore --self-contained --runtime $runtime -o "$lOutputFolder" --framework $framework - dotnet publish -c Release --no-restore --self-contained --runtime $runtime -o "$lOutputFolder" --framework $framework + 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" echo "Renaming API -> Kavita" mv "$lOutputFolder"/API "$lOutputFolder"/Kavita @@ -64,7 +63,7 @@ Package() cd ../$outputFolder/"$runtime"/ tar -czvf ../kavita-$runtime.tar.gz Kavita - ProgressEnd "Creating $runtime Package for $framework" + ProgressEnd "Creating $runtime Package" } @@ -77,15 +76,15 @@ fi #Build for x64 Build "linux-x64" -Package "net6.0" "linux-x64" +Package "linux-x64" cd "$dir" #Build for arm Build "linux-arm" -Package "net6.0" "linux-arm" +Package "linux-arm" cd "$dir" #Build for arm64 Build "linux-arm64" -Package "net6.0" "linux-arm64" +Package "linux-arm64" cd "$dir" diff --git a/openapi.json b/openapi.json index 397ace216..bf4f54b44 100644 --- a/openapi.json +++ b/openapi.json @@ -7,7 +7,7 @@ "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "0.7.1.4" + "version": "0.7.1.5" }, "servers": [ { @@ -11791,16 +11791,16 @@ "type": "boolean", "description": "Include library series in Search" }, - "collapseSeriesRelationships": { - "type": "boolean", - "description": "When showing series, only parent series or series with no relationships will be returned" - }, "folders": { "type": "array", "items": { "type": "string" }, "nullable": true + }, + "collapseSeriesRelationships": { + "type": "boolean", + "description": "When showing series, only parent series or series with no relationships will be returned" } }, "additionalProperties": false @@ -12087,9 +12087,6 @@ "password": { "type": "string", "nullable": true - }, - "sendEmail": { - "type": "boolean" } }, "additionalProperties": false @@ -12495,11 +12492,11 @@ "type": "string", "nullable": true }, - "chapterTitleName": { + "volumeNumber": { "type": "string", "nullable": true }, - "volumeNumber": { + "chapterTitleName": { "type": "string", "nullable": true }, @@ -12511,6 +12508,10 @@ "type": "integer", "format": "int32" }, + "title": { + "type": "string", + "nullable": true + }, "libraryType": { "$ref": "#/components/schemas/LibraryType" }, @@ -12518,10 +12519,6 @@ "type": "string", "nullable": true }, - "title": { - "type": "string", - "nullable": true - }, "releaseDate": { "type": "string", "description": "Release Date from Chapter", @@ -14859,12 +14856,15 @@ "additionalProperties": false }, "UploadUrlDto": { + "required": [ + "url" + ], "type": "object", "properties": { "url": { + "minLength": 1, "type": "string", - "description": "External url", - "nullable": true + "description": "External url" } }, "additionalProperties": false @@ -14938,7 +14938,8 @@ "readingDirection", "scalingOption", "showScreenHints", - "swipeToPaginate" + "swipeToPaginate", + "theme" ], "type": "object", "properties": {