diff --git a/.gitignore b/.gitignore
index c8d68977f..eec036cbe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -517,10 +517,12 @@ UI/Web/dist/
/API/config/kavita.db-shm
/API/config/kavita.db-wal
/API/config/kavita.db-journal
+/API/config/*.db
+/API/config/*.bak
+/API/config/*.backup
/API/config/Hangfire.db
/API/config/Hangfire-log.db
API/config/covers/
-API/config/*.db
API/config/stats/*
API/config/stats/app_stats.json
API/config/pre-metadata/
diff --git a/API.Benchmark/ParseScannedFilesBenchmarks.cs b/API.Benchmark/ParseScannedFilesBenchmarks.cs
index 7c244a5d4..1dcca79b9 100644
--- a/API.Benchmark/ParseScannedFilesBenchmarks.cs
+++ b/API.Benchmark/ParseScannedFilesBenchmarks.cs
@@ -1,5 +1,6 @@
using System.IO;
using System.IO.Abstractions;
+using System.Threading.Tasks;
using API.Entities.Enums;
using API.Parser;
using API.Services;
@@ -46,7 +47,7 @@ namespace API.Benchmark
/// Generate a list of Series and another list with
///
[Benchmark]
- public void MergeName()
+ public async Task MergeName()
{
var libraryPath = Path.Join(Directory.GetCurrentDirectory(),
"../../../Services/Test Data/ScannerService/Manga");
@@ -61,7 +62,7 @@ namespace API.Benchmark
Title = "A Town Where You Live",
Volumes = "1"
};
- _parseScannedFiles.ScanLibrariesForSeries(LibraryType.Manga, new [] {libraryPath}, "Manga");
+ await _parseScannedFiles.ScanLibrariesForSeries(LibraryType.Manga, new [] {libraryPath}, "Manga");
_parseScannedFiles.MergeName(p1);
}
}
diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj
index 3b4b8cb94..708e253c0 100644
--- a/API.Tests/API.Tests.csproj
+++ b/API.Tests/API.Tests.csproj
@@ -7,10 +7,10 @@
-
+
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/API.Tests/Comparers/ChapterSortComparerZeroFirstTests.cs b/API.Tests/Comparers/ChapterSortComparerZeroFirstTests.cs
index 72d6908c6..df3934884 100644
--- a/API.Tests/Comparers/ChapterSortComparerZeroFirstTests.cs
+++ b/API.Tests/Comparers/ChapterSortComparerZeroFirstTests.cs
@@ -14,4 +14,11 @@ public class ChapterSortComparerZeroFirstTests
{
Assert.Equal(expected, input.OrderBy(f => f, new ChapterSortComparerZeroFirst()).ToArray());
}
+
+ [Theory]
+ [InlineData(new[] {1.0, 0.5, 0.3}, new[] {0.3, 0.5, 1.0})]
+ public void ChapterSortComparerZeroFirstTest_Doubles(double[] input, double[] expected)
+ {
+ Assert.Equal(expected, input.OrderBy(f => f, new ChapterSortComparerZeroFirst()).ToArray());
+ }
}
diff --git a/API.Tests/Comparers/NumericComparerTests.cs b/API.Tests/Comparers/NumericComparerTests.cs
index 9a66e7666..8a1f23773 100644
--- a/API.Tests/Comparers/NumericComparerTests.cs
+++ b/API.Tests/Comparers/NumericComparerTests.cs
@@ -11,6 +11,10 @@ public class NumericComparerTests
new[] {"x1.jpg", "x10.jpg", "x3.jpg", "x4.jpg", "x11.jpg"},
new[] {"x1.jpg", "x3.jpg", "x4.jpg", "x10.jpg", "x11.jpg"}
)]
+ [InlineData(
+ new[] {"x1.0.jpg", "0.5.jpg", "0.3.jpg"},
+ new[] {"0.3.jpg", "0.5.jpg", "x1.0.jpg",}
+ )]
public void NumericComparerTest(string[] input, string[] expected)
{
var nc = new NumericComparer();
diff --git a/API.Tests/Entities/ComicInfoTests.cs b/API.Tests/Entities/ComicInfoTests.cs
index 7b7106eb9..325299cf8 100644
--- a/API.Tests/Entities/ComicInfoTests.cs
+++ b/API.Tests/Entities/ComicInfoTests.cs
@@ -16,7 +16,7 @@ public class ComicInfoTests
[InlineData("Early Childhood", AgeRating.EarlyChildhood)]
[InlineData("Everyone 10+", AgeRating.Everyone10Plus)]
[InlineData("M", AgeRating.Mature)]
- [InlineData("MA 15+", AgeRating.Mature15Plus)]
+ [InlineData("MA15+", AgeRating.Mature15Plus)]
[InlineData("Mature 17+", AgeRating.Mature17Plus)]
[InlineData("Rating Pending", AgeRating.RatingPending)]
[InlineData("X18+", AgeRating.X18Plus)]
diff --git a/API.Tests/Extensions/ChapterListExtensionsTests.cs b/API.Tests/Extensions/ChapterListExtensionsTests.cs
index 845b1387b..a1beddf09 100644
--- a/API.Tests/Extensions/ChapterListExtensionsTests.cs
+++ b/API.Tests/Extensions/ChapterListExtensionsTests.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Linq;
using API.Entities;
using API.Entities.Enums;
@@ -83,9 +83,37 @@ namespace API.Tests.Extensions
Assert.Equal(chapterList[0], actualChapter);
}
+ [Fact]
+ public void GetChapterByRange_On_Duplicate_Files_Test_Should_Not_Error()
+ {
+ var info = new ParserInfo()
+ {
+ Chapters = "0",
+ Edition = "",
+ Format = MangaFormat.Archive,
+ FullFilePath = "/manga/detective comics #001.cbz",
+ Filename = "detective comics #001.cbz",
+ IsSpecial = true,
+ Series = "detective comics",
+ Title = "detective comics",
+ Volumes = "0"
+ };
+
+ var chapterList = new List()
+ {
+ CreateChapter("detective comics", "0", CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true),
+ CreateChapter("detective comics", "0", CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true)
+ };
+
+ var actualChapter = chapterList.GetChapterByRange(info);
+
+ Assert.Equal(chapterList[0], actualChapter);
+
+ }
+
#region GetFirstChapterWithFiles
- [Fact]
+ [Fact]
public void GetFirstChapterWithFiles_ShouldReturnAllChapters()
{
var chapterList = new List()
diff --git a/API.Tests/Extensions/VolumeListExtensionsTests.cs b/API.Tests/Extensions/VolumeListExtensionsTests.cs
index 48d39aa24..264437ecd 100644
--- a/API.Tests/Extensions/VolumeListExtensionsTests.cs
+++ b/API.Tests/Extensions/VolumeListExtensionsTests.cs
@@ -9,67 +9,6 @@ namespace API.Tests.Extensions;
public class VolumeListExtensionsTests
{
- #region FirstWithChapters
-
- [Fact]
- public void FirstWithChapters_ReturnsVolumeWithChapters()
- {
- var volumes = new List()
- {
- EntityFactory.CreateVolume("0", new List()),
- EntityFactory.CreateVolume("1", new List()
- {
- EntityFactory.CreateChapter("1", false),
- EntityFactory.CreateChapter("2", false),
- }),
- };
-
- Assert.Equal(volumes[1].Number, volumes.FirstWithChapters(false).Number);
- Assert.Equal(volumes[1].Number, volumes.FirstWithChapters(true).Number);
- }
-
- [Fact]
- public void FirstWithChapters_Book()
- {
- var volumes = new List()
- {
- EntityFactory.CreateVolume("1", new List()
- {
- EntityFactory.CreateChapter("3", false),
- EntityFactory.CreateChapter("4", false),
- }),
- EntityFactory.CreateVolume("0", new List()
- {
- EntityFactory.CreateChapter("1", false),
- EntityFactory.CreateChapter("0", true),
- }),
- };
-
- Assert.Equal(volumes[0].Number, volumes.FirstWithChapters(true).Number);
- }
-
- [Fact]
- public void FirstWithChapters_NonBook()
- {
- var volumes = new List()
- {
- EntityFactory.CreateVolume("1", new List()
- {
- EntityFactory.CreateChapter("3", false),
- EntityFactory.CreateChapter("4", false),
- }),
- EntityFactory.CreateVolume("0", new List()
- {
- EntityFactory.CreateChapter("1", false),
- EntityFactory.CreateChapter("0", true),
- }),
- };
-
- Assert.Equal(volumes[0].Number, volumes.FirstWithChapters(false).Number);
- }
-
- #endregion
-
#region GetCoverImage
[Fact]
diff --git a/API.Tests/Helpers/EntityFactory.cs b/API.Tests/Helpers/EntityFactory.cs
index e98cd5730..3632ff9a0 100644
--- a/API.Tests/Helpers/EntityFactory.cs
+++ b/API.Tests/Helpers/EntityFactory.cs
@@ -31,7 +31,7 @@ namespace API.Tests.Helpers
return new Volume()
{
Name = volumeNumber,
- Number = (int) API.Parser.Parser.MinimumNumberFromRange(volumeNumber),
+ Number = (int) API.Parser.Parser.MinNumberFromRange(volumeNumber),
Pages = pages,
Chapters = chaps
};
@@ -43,7 +43,7 @@ namespace API.Tests.Helpers
{
IsSpecial = isSpecial,
Range = range,
- Number = API.Parser.Parser.MinimumNumberFromRange(range) + string.Empty,
+ Number = API.Parser.Parser.MinNumberFromRange(range) + string.Empty,
Files = files ?? new List(),
Pages = pageCount,
diff --git a/API.Tests/Parser/DefaultParserTests.cs b/API.Tests/Parser/DefaultParserTests.cs
index 200a6b16a..f32838dd3 100644
--- a/API.Tests/Parser/DefaultParserTests.cs
+++ b/API.Tests/Parser/DefaultParserTests.cs
@@ -122,7 +122,7 @@ public class DefaultParserTests
filepath = @"E:\Manga\Tenjo Tenge (Color)\Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz";
expected.Add(filepath, new ParserInfo
{
- Series = "Tenjo Tenge", Volumes = "1", Edition = "Full Contact Edition",
+ Series = "Tenjo Tenge {Full Contact Edition}", Volumes = "1", Edition = "",
Chapters = "0", Filename = "Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz", Format = MangaFormat.Archive,
FullFilePath = filepath
});
diff --git a/API.Tests/Parser/MangaParserTests.cs b/API.Tests/Parser/MangaParserTests.cs
index 34c41015c..a3d298e82 100644
--- a/API.Tests/Parser/MangaParserTests.cs
+++ b/API.Tests/Parser/MangaParserTests.cs
@@ -169,6 +169,8 @@ namespace API.Tests.Parser
[InlineData("Love Hina - Volume 01 [Scans].pdf", "Love Hina")]
[InlineData("It's Witching Time! 001 (Digital) (Anonymous1234)", "It's Witching Time!")]
[InlineData("Zettai Karen Children v02 c003 - The Invisible Guardian (2) [JS Scans]", "Zettai Karen Children")]
+ [InlineData("My Charms Are Wasted on Kuroiwa Medaka - Ch. 37.5 - Volume Extras", "My Charms Are Wasted on Kuroiwa Medaka")]
+ [InlineData("Highschool of the Dead - Full Color Edition v02 [Uasaha] (Yen Press)", "Highschool of the Dead - Full Color Edition")]
public void ParseSeriesTest(string filename, string expected)
{
Assert.Equal(expected, API.Parser.Parser.ParseSeries(filename));
@@ -252,13 +254,13 @@ namespace API.Tests.Parser
[Theory]
[InlineData("Tenjou Tenge Omnibus", "Omnibus")]
- [InlineData("Tenjou Tenge {Full Contact Edition}", "Full Contact Edition")]
- [InlineData("Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz", "Full Contact Edition")]
+ [InlineData("Tenjou Tenge {Full Contact Edition}", "")]
+ [InlineData("Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz", "")]
[InlineData("Wotakoi - Love is Hard for Otaku Omnibus v01 (2018) (Digital) (danke-Empire)", "Omnibus")]
[InlineData("To Love Ru v01 Uncensored (Ch.001-007)", "Uncensored")]
[InlineData("Chobits Omnibus Edition v01 [Dark Horse]", "Omnibus Edition")]
[InlineData("[dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz", "")]
- [InlineData("AKIRA - c003 (v01) [Full Color] [Darkhorse].cbz", "Full Color")]
+ [InlineData("AKIRA - c003 (v01) [Full Color] [Darkhorse].cbz", "")]
[InlineData("Love Hina Omnibus v05 (2015) (Digital-HD) (Asgard-Empire).cbz", "Omnibus")]
public void ParseEditionTest(string input, string expected)
{
diff --git a/API.Tests/Parser/ParserTest.cs b/API.Tests/Parser/ParserTest.cs
index ff1b4dc71..4ae75d91b 100644
--- a/API.Tests/Parser/ParserTest.cs
+++ b/API.Tests/Parser/ParserTest.cs
@@ -63,6 +63,7 @@ namespace API.Tests.Parser
[InlineData("- The Title", false, "The Title")]
[InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1", false, "Kasumi Otoko no Ko v1.1")]
[InlineData("Batman - Detective Comics - Rebirth Deluxe Edition Book 04 (2019) (digital) (Son of Ultron-Empire)", true, "Batman - Detective Comics - Rebirth Deluxe Edition")]
+ [InlineData("Something - Full Color Edition", false, "Something - Full Color Edition")]
public void CleanTitleTest(string input, bool isComic, string expected)
{
Assert.Equal(expected, CleanTitle(input, isComic));
@@ -139,7 +140,7 @@ namespace API.Tests.Parser
[InlineData("40.1_a", 0)]
public void MinimumNumberFromRangeTest(string input, float expected)
{
- Assert.Equal(expected, MinimumNumberFromRange(input));
+ Assert.Equal(expected, MinNumberFromRange(input));
}
[Theory]
@@ -152,7 +153,7 @@ namespace API.Tests.Parser
[InlineData("40.1_a", 0)]
public void MaximumNumberFromRangeTest(string input, float expected)
{
- Assert.Equal(expected, MaximumNumberFromRange(input));
+ Assert.Equal(expected, MaxNumberFromRange(input));
}
[Theory]
@@ -179,6 +180,7 @@ namespace API.Tests.Parser
[InlineData(".test.jpg", false)]
[InlineData("!test.jpg", true)]
[InlineData("test.webp", true)]
+ [InlineData("test.gif", true)]
public void IsImageTest(string filename, bool expected)
{
Assert.Equal(expected, IsImage(filename));
@@ -197,6 +199,7 @@ namespace API.Tests.Parser
[InlineData("ch1/cover.png", true)]
[InlineData("ch1/backcover.png", false)]
[InlineData("backcover.png", false)]
+ [InlineData("back_cover.png", false)]
public void IsCoverImageTest(string inputPath, bool expected)
{
Assert.Equal(expected, IsCoverImage(inputPath));
diff --git a/API.Tests/Services/BookmarkServiceTests.cs b/API.Tests/Services/BookmarkServiceTests.cs
index 0026ea678..13acf3684 100644
--- a/API.Tests/Services/BookmarkServiceTests.cs
+++ b/API.Tests/Services/BookmarkServiceTests.cs
@@ -389,7 +389,7 @@ public class BookmarkServiceTests
VolumeId = 1
}, $"{CacheDirectory}1/0001.jpg");
- var files = await bookmarkService.GetBookmarkFilesById(1, new[] {1});
+ var files = await bookmarkService.GetBookmarkFilesById(new[] {1});
var actualFiles = ds.GetFiles(BookmarkDirectory, searchOption: SearchOption.AllDirectories);
Assert.Equal(files.Select(API.Parser.Parser.NormalizePath).ToList(), actualFiles.Select(API.Parser.Parser.NormalizePath).ToList());
}
diff --git a/API.Tests/Services/CacheServiceTests.cs b/API.Tests/Services/CacheServiceTests.cs
index 7a2cbada8..d5a8d4bee 100644
--- a/API.Tests/Services/CacheServiceTests.cs
+++ b/API.Tests/Services/CacheServiceTests.cs
@@ -157,7 +157,8 @@ namespace API.Tests.Services
filesystem.AddDirectory($"{CacheDirectory}1/");
var ds = new DirectoryService(Substitute.For>(), filesystem);
var cleanupService = new CacheService(_logger, _unitOfWork, ds,
- new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds));
+ new ReadingItemService(Substitute.For(),
+ Substitute.For(), Substitute.For(), ds), Substitute.For());
await ResetDB();
var s = DbFactory.Series("Test");
@@ -240,7 +241,8 @@ namespace API.Tests.Services
filesystem.AddFile($"{CacheDirectory}3/003.jpg", new MockFileData(""));
var ds = new DirectoryService(Substitute.For>(), filesystem);
var cleanupService = new CacheService(_logger, _unitOfWork, ds,
- new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds));
+ new ReadingItemService(Substitute.For(),
+ Substitute.For(), Substitute.For(), ds), Substitute.For());
cleanupService.CleanupChapters(new []{1, 3});
Assert.Empty(ds.GetFiles(CacheDirectory, searchOption:SearchOption.AllDirectories));
@@ -260,7 +262,8 @@ namespace API.Tests.Services
filesystem.AddFile($"{DataDirectory}2.epub", new MockFileData(""));
var ds = new DirectoryService(Substitute.For>(), filesystem);
var cs = new CacheService(_logger, _unitOfWork, ds,
- new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds));
+ new ReadingItemService(Substitute.For(),
+ Substitute.For(), Substitute.For(), ds), Substitute.For());
var c = new Chapter()
{
@@ -311,7 +314,8 @@ namespace API.Tests.Services
var ds = new DirectoryService(Substitute.For>(), filesystem);
var cs = new CacheService(_logger, _unitOfWork, ds,
- new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds));
+ new ReadingItemService(Substitute.For(),
+ Substitute.For(), Substitute.For(), ds), Substitute.For());
// Flatten to prepare for how GetFullPath expects
ds.Flatten($"{CacheDirectory}1/");
@@ -362,7 +366,8 @@ namespace API.Tests.Services
var ds = new DirectoryService(Substitute.For>(), filesystem);
var cs = new CacheService(_logger, _unitOfWork, ds,
- new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds));
+ new ReadingItemService(Substitute.For(),
+ Substitute.For(), Substitute.For(), ds), Substitute.For());
// Flatten to prepare for how GetFullPath expects
ds.Flatten($"{CacheDirectory}1/");
@@ -408,7 +413,8 @@ namespace API.Tests.Services
var ds = new DirectoryService(Substitute.For>(), filesystem);
var cs = new CacheService(_logger, _unitOfWork, ds,
- new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds));
+ new ReadingItemService(Substitute.For(),
+ Substitute.For(), Substitute.For(), ds), Substitute.For());
// Flatten to prepare for how GetFullPath expects
ds.Flatten($"{CacheDirectory}1/");
@@ -460,7 +466,8 @@ namespace API.Tests.Services
var ds = new DirectoryService(Substitute.For>(), filesystem);
var cs = new CacheService(_logger, _unitOfWork, ds,
- new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds));
+ new ReadingItemService(Substitute.For(),
+ Substitute.For(), Substitute.For(), ds), Substitute.For());
// Flatten to prepare for how GetFullPath expects
ds.Flatten($"{CacheDirectory}1/");
diff --git a/API.Tests/Services/CleanupServiceTests.cs b/API.Tests/Services/CleanupServiceTests.cs
index 1dd131112..08d5f29a7 100644
--- a/API.Tests/Services/CleanupServiceTests.cs
+++ b/API.Tests/Services/CleanupServiceTests.cs
@@ -11,6 +11,7 @@ using API.Entities.Enums;
using API.Services;
using API.Services.Tasks;
using API.SignalR;
+using API.Tests.Helpers;
using AutoMapper;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Data.Sqlite;
@@ -125,23 +126,23 @@ public class CleanupServiceTests
public async Task DeleteSeriesCoverImages_ShouldDeleteAll()
{
var filesystem = CreateFileSystem();
- filesystem.AddFile($"{CoverImageDirectory}series_01.jpg", new MockFileData(""));
- filesystem.AddFile($"{CoverImageDirectory}series_03.jpg", new MockFileData(""));
- filesystem.AddFile($"{CoverImageDirectory}series_1000.jpg", new MockFileData(""));
+ filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(1)}.jpg", new MockFileData(""));
+ filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(3)}.jpg", new MockFileData(""));
+ filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(1000)}.jpg", new MockFileData(""));
// Delete all Series to reset state
await ResetDB();
var s = DbFactory.Series("Test 1");
- s.CoverImage = "series_01.jpg";
+ s.CoverImage = $"{ImageService.GetSeriesFormat(1)}.jpg";
s.LibraryId = 1;
_context.Series.Add(s);
s = DbFactory.Series("Test 2");
- s.CoverImage = "series_03.jpg";
+ s.CoverImage = $"{ImageService.GetSeriesFormat(3)}.jpg";
s.LibraryId = 1;
_context.Series.Add(s);
s = DbFactory.Series("Test 3");
- s.CoverImage = "series_1000.jpg";
+ s.CoverImage = $"{ImageService.GetSeriesFormat(1000)}.jpg";
s.LibraryId = 1;
_context.Series.Add(s);
@@ -158,20 +159,20 @@ public class CleanupServiceTests
public async Task DeleteSeriesCoverImages_ShouldNotDeleteLinkedFiles()
{
var filesystem = CreateFileSystem();
- filesystem.AddFile($"{CoverImageDirectory}series_01.jpg", new MockFileData(""));
- filesystem.AddFile($"{CoverImageDirectory}series_03.jpg", new MockFileData(""));
- filesystem.AddFile($"{CoverImageDirectory}series_1000.jpg", new MockFileData(""));
+ filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(1)}.jpg", new MockFileData(""));
+ filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(3)}.jpg", new MockFileData(""));
+ filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(1000)}.jpg", new MockFileData(""));
// Delete all Series to reset state
await ResetDB();
// Add 2 series with cover images
var s = DbFactory.Series("Test 1");
- s.CoverImage = "series_01.jpg";
+ s.CoverImage = $"{ImageService.GetSeriesFormat(1)}.jpg";
s.LibraryId = 1;
_context.Series.Add(s);
s = DbFactory.Series("Test 2");
- s.CoverImage = "series_03.jpg";
+ s.CoverImage = $"{ImageService.GetSeriesFormat(3)}.jpg";
s.LibraryId = 1;
_context.Series.Add(s);
@@ -242,9 +243,9 @@ public class CleanupServiceTests
public async Task DeleteTagCoverImages_ShouldNotDeleteLinkedFiles()
{
var filesystem = CreateFileSystem();
- filesystem.AddFile($"{CoverImageDirectory}tag_01.jpg", new MockFileData(""));
- filesystem.AddFile($"{CoverImageDirectory}tag_02.jpg", new MockFileData(""));
- filesystem.AddFile($"{CoverImageDirectory}tag_1000.jpg", new MockFileData(""));
+ filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetCollectionTagFormat(1)}.jpg", new MockFileData(""));
+ filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetCollectionTagFormat(2)}.jpg", new MockFileData(""));
+ filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetCollectionTagFormat(1000)}.jpg", new MockFileData(""));
// Delete all Series to reset state
await ResetDB();
@@ -255,9 +256,9 @@ public class CleanupServiceTests
s.Metadata.CollectionTags.Add(new CollectionTag()
{
Title = "Something",
- CoverImage ="tag_01.jpg"
+ CoverImage = $"{ImageService.GetCollectionTagFormat(1)}.jpg"
});
- s.CoverImage = "series_01.jpg";
+ s.CoverImage = $"{ImageService.GetSeriesFormat(1)}.jpg";
s.LibraryId = 1;
_context.Series.Add(s);
@@ -266,9 +267,9 @@ public class CleanupServiceTests
s.Metadata.CollectionTags.Add(new CollectionTag()
{
Title = "Something 2",
- CoverImage ="tag_02.jpg"
+ CoverImage = $"{ImageService.GetCollectionTagFormat(2)}.jpg"
});
- s.CoverImage = "series_03.jpg";
+ s.CoverImage = $"{ImageService.GetSeriesFormat(3)}.jpg";
s.LibraryId = 1;
_context.Series.Add(s);
@@ -285,6 +286,49 @@ public class CleanupServiceTests
#endregion
+ #region DeleteReadingListCoverImages
+ [Fact]
+ public async Task DeleteReadingListCoverImages_ShouldNotDeleteLinkedFiles()
+ {
+ var filesystem = CreateFileSystem();
+ filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetReadingListFormat(1)}.jpg", new MockFileData(""));
+ filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetReadingListFormat(2)}.jpg", new MockFileData(""));
+ filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetReadingListFormat(3)}.jpg", new MockFileData(""));
+
+ // Delete all Series to reset state
+ await ResetDB();
+
+ _context.Users.Add(new AppUser()
+ {
+ UserName = "Joe",
+ ReadingLists = new List()
+ {
+ new ReadingList()
+ {
+ Title = "Something",
+ NormalizedTitle = API.Parser.Parser.Normalize("Something"),
+ CoverImage = $"{ImageService.GetReadingListFormat(1)}.jpg"
+ },
+ new ReadingList()
+ {
+ Title = "Something 2",
+ NormalizedTitle = API.Parser.Parser.Normalize("Something 2"),
+ CoverImage = $"{ImageService.GetReadingListFormat(2)}.jpg"
+ }
+ }
+ });
+
+ await _context.SaveChangesAsync();
+ var ds = new DirectoryService(Substitute.For>(), filesystem);
+ var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub,
+ ds);
+
+ await cleanupService.DeleteReadingListCoverImages();
+
+ Assert.Equal(2, ds.GetFiles(CoverImageDirectory).Count());
+ }
+ #endregion
+
#region CleanupCacheDirectory
[Fact]
@@ -320,7 +364,7 @@ public class CleanupServiceTests
#region CleanupBackups
[Fact]
- public void CleanupBackups_LeaveOneFile_SinceAllAreExpired()
+ public async Task CleanupBackups_LeaveOneFile_SinceAllAreExpired()
{
var filesystem = CreateFileSystem();
var filesystemFile = new MockFileData("")
@@ -334,12 +378,12 @@ public class CleanupServiceTests
var ds = new DirectoryService(Substitute.For>(), filesystem);
var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub,
ds);
- cleanupService.CleanupBackups();
+ await cleanupService.CleanupBackups();
Assert.Single(ds.GetFiles(BackupDirectory, searchOption: SearchOption.AllDirectories));
}
[Fact]
- public void CleanupBackups_LeaveLestExpired()
+ public async Task CleanupBackups_LeaveLestExpired()
{
var filesystem = CreateFileSystem();
var filesystemFile = new MockFileData("")
@@ -356,7 +400,7 @@ public class CleanupServiceTests
var ds = new DirectoryService(Substitute.For>(), filesystem);
var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub,
ds);
- cleanupService.CleanupBackups();
+ await cleanupService.CleanupBackups();
Assert.True(filesystem.File.Exists($"{BackupDirectory}randomfile.zip"));
}
diff --git a/API.Tests/Services/DirectoryServiceTests.cs b/API.Tests/Services/DirectoryServiceTests.cs
index 9a4bef08e..23a7dfad1 100644
--- a/API.Tests/Services/DirectoryServiceTests.cs
+++ b/API.Tests/Services/DirectoryServiceTests.cs
@@ -17,6 +17,7 @@ namespace API.Tests.Services
{
private readonly ILogger _logger = Substitute.For>();
+
#region TraverseTreeParallelForEach
[Fact]
public void TraverseTreeParallelForEach_JustArchives_ShouldBe28()
@@ -575,19 +576,22 @@ namespace API.Tests.Services
[Fact]
public void CopyFilesToDirectory_ShouldAppendWhenTargetFileExists()
{
+
const string testDirectory = "/manga/";
var fileSystem = new MockFileSystem();
- fileSystem.AddFile($"{testDirectory}file.zip", new MockFileData(""));
- fileSystem.AddFile($"/manga/output/file (1).zip", new MockFileData(""));
- fileSystem.AddFile($"/manga/output/file (2).zip", new MockFileData(""));
+ fileSystem.AddFile(MockUnixSupport.Path($"{testDirectory}file.zip"), new MockFileData(""));
+ fileSystem.AddFile(MockUnixSupport.Path($"/manga/output/file (1).zip"), new MockFileData(""));
+ fileSystem.AddFile(MockUnixSupport.Path($"/manga/output/file (2).zip"), new MockFileData(""));
var ds = new DirectoryService(Substitute.For>(), fileSystem);
- ds.CopyFilesToDirectory(new []{$"{testDirectory}file.zip"}, "/manga/output/");
- ds.CopyFilesToDirectory(new []{$"{testDirectory}file.zip"}, "/manga/output/");
+ 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.Parser.Parser.NormalizePath).ToList();
Assert.Equal(4, outputFiles.Count()); // we have 2 already there and 2 copies
- // For some reason, this has C:/ on directory even though everything is emulated
- Assert.True(outputFiles.Contains(API.Parser.Parser.NormalizePath("/manga/output/file (3).zip")) || outputFiles.Contains(API.Parser.Parser.NormalizePath("C:/manga/output/file (3).zip")));
+ // 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.Parser.Parser.NormalizePath("/manga/output/file (3).zip"))
+ || outputFiles.Contains(API.Parser.Parser.NormalizePath("C:/manga/output/file (3).zip")));
}
#endregion
diff --git a/API.Tests/Services/ReaderServiceTests.cs b/API.Tests/Services/ReaderServiceTests.cs
index 8439c69d3..cfb89935a 100644
--- a/API.Tests/Services/ReaderServiceTests.cs
+++ b/API.Tests/Services/ReaderServiceTests.cs
@@ -11,6 +11,7 @@ using API.Entities;
using API.Entities.Enums;
using API.Helpers;
using API.Services;
+using API.SignalR;
using API.Tests.Helpers;
using AutoMapper;
using Microsoft.Data.Sqlite;
@@ -147,7 +148,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
Assert.Equal(0, await readerService.CapPageToChapter(1, -1));
Assert.Equal(1, await readerService.CapPageToChapter(1, 10));
@@ -191,7 +192,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var successful = await readerService.SaveReadingProgress(new ProgressDto()
{
@@ -240,7 +241,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var successful = await readerService.SaveReadingProgress(new ProgressDto()
{
@@ -310,7 +311,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var volumes = await _unitOfWork.VolumeRepository.GetVolumes(1);
readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
@@ -360,7 +361,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(1)).ToList();
readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
@@ -420,7 +421,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 1, 1);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
@@ -466,7 +467,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1);
@@ -508,7 +509,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var nextChapter = await readerService.GetNextChapterIdAsync(1, 2, 4, 1);
@@ -551,7 +552,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var nextChapter = await readerService.GetNextChapterIdAsync(1, 2, 4, 1);
@@ -587,7 +588,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1);
@@ -628,7 +629,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1);
@@ -669,7 +670,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1);
@@ -708,7 +709,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1);
@@ -751,7 +752,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 3, 1);
@@ -793,7 +794,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var nextChapter = await readerService.GetNextChapterIdAsync(1, 2, 3, 1);
@@ -846,7 +847,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 2, 1);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
@@ -892,7 +893,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 2, 3, 1);
@@ -934,7 +935,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 2, 3, 1);
@@ -972,7 +973,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
@@ -1007,7 +1008,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
@@ -1047,7 +1048,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
@@ -1095,7 +1096,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 2,5, 1);
var chapterInfoDto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(prevChapter);
@@ -1137,7 +1138,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
@@ -1178,7 +1179,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 2, 4, 1);
@@ -1221,7 +1222,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
@@ -1276,7 +1277,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var nextChapter = await readerService.GetContinuePoint(1, 1);
@@ -1321,7 +1322,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
// Save progress on first volume chapters and 1st of second volume
await readerService.SaveReadingProgress(new ProgressDto()
@@ -1404,7 +1405,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
// Save progress on first volume and 1st chapter of second volume
await readerService.SaveReadingProgress(new ProgressDto()
@@ -1470,7 +1471,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
// Save progress on first volume chapters and 1st of second volume
await readerService.SaveReadingProgress(new ProgressDto()
@@ -1538,7 +1539,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var nextChapter = await readerService.GetContinuePoint(1, 1);
Assert.Equal("1", nextChapter.Range);
@@ -1575,7 +1576,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
// Save progress on first volume chapters and 1st of second volume
await readerService.SaveReadingProgress(new ProgressDto()
@@ -1640,7 +1641,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
// Save progress on first volume chapters and 1st of second volume
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress);
@@ -1681,7 +1682,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
// Save progress on first volume chapters and 1st of second volume
await readerService.SaveReadingProgress(new ProgressDto()
@@ -1753,7 +1754,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress);
await readerService.MarkSeriesAsRead(user, 1);
await _context.SaveChangesAsync();
@@ -1801,7 +1802,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
await readerService.MarkChaptersUntilAsRead(user, 1, 5);
@@ -1844,7 +1845,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
await readerService.MarkChaptersUntilAsRead(user, 1, 2.5f);
@@ -1888,7 +1889,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
await readerService.MarkChaptersUntilAsRead(user, 1, 2);
@@ -1947,7 +1948,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress);
const int markReadUntilNumber = 47;
@@ -2027,7 +2028,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
await readerService.MarkSeriesAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1);
await _context.SaveChangesAsync();
@@ -2078,7 +2079,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync();
- var readerService = new ReaderService(_unitOfWork, Substitute.For>());
+ var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For());
var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(1)).ToList();
readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters);
diff --git a/API.Tests/Services/ScannerServiceTests.cs b/API.Tests/Services/ScannerServiceTests.cs
index 280fe5c10..e3331bf6d 100644
--- a/API.Tests/Services/ScannerServiceTests.cs
+++ b/API.Tests/Services/ScannerServiceTests.cs
@@ -125,5 +125,6 @@ namespace API.Tests.Services
// }
+
}
}
diff --git a/API.Tests/Services/SeriesServiceTests.cs b/API.Tests/Services/SeriesServiceTests.cs
index 1b9f5fd3f..217eb63e0 100644
--- a/API.Tests/Services/SeriesServiceTests.cs
+++ b/API.Tests/Services/SeriesServiceTests.cs
@@ -22,6 +22,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
+using Xunit.Sdk;
namespace API.Tests.Services;
@@ -703,6 +704,85 @@ public class SeriesServiceTests
Assert.False(series.Metadata.GenresLocked); // GenreLocked is false unless the UI Explicitly says it should be locked
}
+ [Fact]
+ public async Task UpdateSeriesMetadata_ShouldAddNewPerson_NoExistingPeople()
+ {
+ await ResetDb();
+ var s = new Series()
+ {
+ Name = "Test",
+ Library = new Library()
+ {
+ Name = "Test LIb",
+ Type = LibraryType.Book,
+ },
+ Metadata = DbFactory.SeriesMetadata(new List())
+ };
+ var g = DbFactory.Person("Existing Person", PersonRole.Publisher);
+ _context.Series.Add(s);
+
+ _context.Person.Add(g);
+ await _context.SaveChangesAsync();
+
+ var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto()
+ {
+ SeriesMetadata = new SeriesMetadataDto()
+ {
+ SeriesId = 1,
+ Publishers = new List() {new () {Id = 0, Name = "Existing Person", Role = PersonRole.Publisher}},
+ },
+ CollectionTags = new List()
+ });
+
+ Assert.True(success);
+
+ var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
+ Assert.NotNull(series.Metadata);
+ Assert.True(series.Metadata.People.Select(g => g.Name).All(g => g == "Existing Person"));
+ Assert.False(series.Metadata.PublisherLocked); // PublisherLocked is false unless the UI Explicitly says it should be locked
+ }
+
+ [Fact]
+ public async Task UpdateSeriesMetadata_ShouldAddNewPerson_ExistingPeople()
+ {
+ await ResetDb();
+ var s = new Series()
+ {
+ Name = "Test",
+ Library = new Library()
+ {
+ Name = "Test LIb",
+ Type = LibraryType.Book,
+ },
+ Metadata = DbFactory.SeriesMetadata(new List())
+ };
+ var g = DbFactory.Person("Existing Person", PersonRole.Publisher);
+ s.Metadata.People = new List() {DbFactory.Person("Existing Writer", PersonRole.Writer),
+ DbFactory.Person("Existing Translator", PersonRole.Translator), DbFactory.Person("Existing Publisher 2", PersonRole.Publisher)};
+ _context.Series.Add(s);
+
+ _context.Person.Add(g);
+ await _context.SaveChangesAsync();
+
+ var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto()
+ {
+ SeriesMetadata = new SeriesMetadataDto()
+ {
+ SeriesId = 1,
+ Publishers = new List() {new () {Id = 0, Name = "Existing Person", Role = PersonRole.Publisher}},
+ PublishersLocked = true
+ },
+ CollectionTags = new List()
+ });
+
+ Assert.True(success);
+
+ var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
+ Assert.NotNull(series.Metadata);
+ Assert.True(series.Metadata.People.Select(g => g.Name).All(g => g == "Existing Person"));
+ Assert.True(series.Metadata.PublisherLocked);
+ }
+
[Fact]
public async Task UpdateSeriesMetadata_ShouldLockIfTold()
{
@@ -745,4 +825,86 @@ public class SeriesServiceTests
}
#endregion
+
+ #region GetFirstChapterForMetadata
+
+ private static Series CreateSeriesMock()
+ {
+ var files = new List()
+ {
+ 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),
+ }),
+ }
+ };
+ }
+
+ [Fact]
+ public void GetFirstChapterForMetadata_Book_Test()
+ {
+ var series = CreateSeriesMock();
+
+ var firstChapter = SeriesService.GetFirstChapterForMetadata(series, true);
+ Assert.Same("1", firstChapter.Range);
+ }
+
+ [Fact]
+ public void GetFirstChapterForMetadata_NonBook_ShouldReturnVolume1()
+ {
+ var series = CreateSeriesMock();
+
+ var firstChapter = SeriesService.GetFirstChapterForMetadata(series, false);
+ Assert.Same("1", firstChapter.Range);
+ }
+
+ [Fact]
+ public void GetFirstChapterForMetadata_NonBook_ShouldReturnVolume1_WhenFirstChapterIsFloat()
+ {
+ var series = CreateSeriesMock();
+ var files = new List()
+ {
+ EntityFactory.CreateMangaFile("Test.cbz", MangaFormat.Archive, 1)
+ };
+ series.Volumes[1].Chapters = new List()
+ {
+ EntityFactory.CreateChapter("2", false, files, 1),
+ EntityFactory.CreateChapter("1.1", false, files, 1),
+ EntityFactory.CreateChapter("1.2", false, files, 1),
+ };
+
+ var firstChapter = SeriesService.GetFirstChapterForMetadata(series, false);
+ Assert.Same("1.1", firstChapter.Range);
+ }
+
+ #endregion
}
diff --git a/API.Tests/Services/SiteThemeServiceTests.cs b/API.Tests/Services/SiteThemeServiceTests.cs
index 3f3f18acf..246461fc8 100644
--- a/API.Tests/Services/SiteThemeServiceTests.cs
+++ b/API.Tests/Services/SiteThemeServiceTests.cs
@@ -25,7 +25,7 @@ namespace API.Tests.Services;
public class SiteThemeServiceTests
{
- private readonly ILogger _logger = Substitute.For>();
+ private readonly ILogger _logger = Substitute.For>();
private readonly IEventHub _messageHub = Substitute.For();
private readonly DbConnection _connection;
@@ -87,7 +87,7 @@ public class SiteThemeServiceTests
UserName = "Joe",
UserPreferences = new AppUserPreferences
{
- Theme = Seed.DefaultThemes[1]
+ Theme = Seed.DefaultThemes[0]
}
});
@@ -135,7 +135,7 @@ public class SiteThemeServiceTests
var filesystem = CreateFileSystem();
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData(""));
var ds = new DirectoryService(Substitute.For>(), filesystem);
- var siteThemeService = new SiteThemeService(ds, _unitOfWork, _messageHub);
+ var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub);
await siteThemeService.Scan();
Assert.NotNull(await _unitOfWork.SiteThemeRepository.GetThemeDtoByName("custom"));
@@ -148,7 +148,7 @@ public class SiteThemeServiceTests
var filesystem = CreateFileSystem();
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData(""));
var ds = new DirectoryService(Substitute.For>(), filesystem);
- var siteThemeService = new SiteThemeService(ds, _unitOfWork, _messageHub);
+ var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub);
await siteThemeService.Scan();
Assert.NotNull(await _unitOfWork.SiteThemeRepository.GetThemeDtoByName("custom"));
@@ -167,7 +167,7 @@ public class SiteThemeServiceTests
var filesystem = CreateFileSystem();
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData(""));
var ds = new DirectoryService(Substitute.For>(), filesystem);
- var siteThemeService = new SiteThemeService(ds, _unitOfWork, _messageHub);
+ var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub);
await siteThemeService.Scan();
Assert.NotNull(await _unitOfWork.SiteThemeRepository.GetThemeDtoByName("custom"));
@@ -188,7 +188,7 @@ public class SiteThemeServiceTests
var filesystem = CreateFileSystem();
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123"));
var ds = new DirectoryService(Substitute.For>(), filesystem);
- var siteThemeService = new SiteThemeService(ds, _unitOfWork, _messageHub);
+ var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub);
_context.SiteTheme.Add(new SiteTheme()
{
@@ -213,7 +213,7 @@ public class SiteThemeServiceTests
var filesystem = CreateFileSystem();
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123"));
var ds = new DirectoryService(Substitute.For>(), filesystem);
- var siteThemeService = new SiteThemeService(ds, _unitOfWork, _messageHub);
+ var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub);
_context.SiteTheme.Add(new SiteTheme()
{
@@ -241,7 +241,7 @@ public class SiteThemeServiceTests
var filesystem = CreateFileSystem();
filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123"));
var ds = new DirectoryService(Substitute.For>(), filesystem);
- var siteThemeService = new SiteThemeService(ds, _unitOfWork, _messageHub);
+ var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub);
_context.SiteTheme.Add(new SiteTheme()
{
diff --git a/API.Tests/Services/Test Data/ArchiveService/Archives/macos_native.zip b/API.Tests/Services/Test Data/ArchiveService/Archives/macos_native.zip
index a84f32b35..c47021fd2 100644
Binary files a/API.Tests/Services/Test Data/ArchiveService/Archives/macos_native.zip and b/API.Tests/Services/Test Data/ArchiveService/Archives/macos_native.zip differ
diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/macos_native.zip b/API.Tests/Services/Test Data/ArchiveService/CoverImages/macos_native.zip
index a84f32b35..c47021fd2 100644
Binary files a/API.Tests/Services/Test Data/ArchiveService/CoverImages/macos_native.zip and b/API.Tests/Services/Test Data/ArchiveService/CoverImages/macos_native.zip differ
diff --git a/API/.dockerignore b/API/.dockerignore
deleted file mode 100644
index cd967fc3a..000000000
--- a/API/.dockerignore
+++ /dev/null
@@ -1,25 +0,0 @@
-**/.dockerignore
-**/.env
-**/.git
-**/.gitignore
-**/.project
-**/.settings
-**/.toolstarget
-**/.vs
-**/.vscode
-**/.idea
-**/*.*proj.user
-**/*.dbmdl
-**/*.jfm
-**/azds.yaml
-**/bin
-**/charts
-**/docker-compose*
-**/Dockerfile*
-**/node_modules
-**/npm-debug.log
-**/obj
-**/secrets.dev.yaml
-**/values.dev.yaml
-LICENSE
-README.md
\ No newline at end of file
diff --git a/API/API.csproj b/API/API.csproj
index 492430a44..1099893a3 100644
--- a/API/API.csproj
+++ b/API/API.csproj
@@ -40,39 +40,39 @@
-
-
+
+
-
-
-
+
+
+
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
-
-
+
+
@@ -94,16 +94,12 @@
-
-
-
-
diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs
index dfa30b18d..cc0b66ec1 100644
--- a/API/Controllers/AccountController.cs
+++ b/API/Controllers/AccountController.cs
@@ -353,6 +353,7 @@ namespace API.Controllers
_logger.LogInformation("{User} is inviting {Email} to the server", adminUser.UserName, dto.Email);
// Check if there is an existing invite
+ dto.Email = dto.Email.Trim();
var emailValidationErrors = await _accountService.ValidateEmail(dto.Email);
if (emailValidationErrors.Any())
{
@@ -454,6 +455,11 @@ namespace API.Controllers
{
var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
+ if (user == null)
+ {
+ return BadRequest("The email does not match the registered email");
+ }
+
// Validate Password and Username
var validationErrors = new List();
validationErrors.AddRange(await _accountService.ValidateUsername(dto.Username));
diff --git a/API/Controllers/BookController.cs b/API/Controllers/BookController.cs
index 89b2d3de4..7b4b49a9f 100644
--- a/API/Controllers/BookController.cs
+++ b/API/Controllers/BookController.cs
@@ -40,7 +40,7 @@ namespace API.Controllers
if (dto.SeriesFormat == MangaFormat.Epub)
{
var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId)).First();
- using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath);
+ using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath, BookService.BookReaderOptions);
bookTitle = book.Title;
}
@@ -63,7 +63,7 @@ namespace API.Controllers
public async Task GetBookPageResources(int chapterId, [FromQuery] string file)
{
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
- var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath);
+ using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.BookReaderOptions);
var key = BookService.CleanContentKeys(file);
if (!book.Content.AllFiles.ContainsKey(key)) return BadRequest("File was not found in book");
@@ -87,7 +87,7 @@ namespace API.Controllers
public async Task>> GetBookChapters(int chapterId)
{
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
- using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath);
+ using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.BookReaderOptions);
var mappings = await _bookService.CreateKeyToPageMappingAsync(book);
var navItems = await book.GetNavigationAsync();
@@ -211,8 +211,7 @@ namespace API.Controllers
var chapter = await _cacheService.Ensure(chapterId);
var path = _cacheService.GetCachedEpubFile(chapter.Id, chapter);
-
- using var book = await EpubReader.OpenBookAsync(path);
+ using var book = await EpubReader.OpenBookAsync(path, BookService.BookReaderOptions);
var mappings = await _bookService.CreateKeyToPageMappingAsync(book);
var counter = 0;
diff --git a/API/Controllers/CollectionController.cs b/API/Controllers/CollectionController.cs
index 6abc22955..a98e28952 100644
--- a/API/Controllers/CollectionController.cs
+++ b/API/Controllers/CollectionController.cs
@@ -157,7 +157,7 @@ namespace API.Controllers
tag.CoverImageLocked = false;
tag.CoverImage = string.Empty;
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
- MessageFactory.CoverUpdateEvent(tag.Id, "collectionTag"), false);
+ MessageFactory.CoverUpdateEvent(tag.Id, MessageFactoryEntityTypes.CollectionTag), false);
_unitOfWork.CollectionTagRepository.Update(tag);
}
diff --git a/API/Controllers/DownloadController.cs b/API/Controllers/DownloadController.cs
index b60fae6e8..433f16721 100644
--- a/API/Controllers/DownloadController.cs
+++ b/API/Controllers/DownloadController.cs
@@ -171,7 +171,7 @@ namespace API.Controllers
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(downloadBookmarkDto.Bookmarks.First().SeriesId);
- var files = await _bookmarkService.GetBookmarkFilesById(user.Id, downloadBookmarkDto.Bookmarks.Select(b => b.Id));
+ var files = await _bookmarkService.GetBookmarkFilesById(downloadBookmarkDto.Bookmarks.Select(b => b.Id));
var filename = $"{series.Name} - Bookmarks.zip";
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
diff --git a/API/Controllers/ImageController.cs b/API/Controllers/ImageController.cs
index 8b58fe9b3..2393d0ea6 100644
--- a/API/Controllers/ImageController.cs
+++ b/API/Controllers/ImageController.cs
@@ -88,6 +88,22 @@ namespace API.Controllers
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
}
+ ///
+ /// Returns cover image for a Reading List
+ ///
+ ///
+ ///
+ [HttpGet("readinglist-cover")]
+ public async Task GetReadingListCoverImage(int readingListId)
+ {
+ var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ReadingListRepository.GetCoverImageAsync(readingListId));
+ if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
+ var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
+
+ Response.AddCacheHeader(path);
+ return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
+ }
+
///
/// Returns image for a given bookmark page
///
diff --git a/API/Controllers/MetadataController.cs b/API/Controllers/MetadataController.cs
index 39c7264d2..ea87456c0 100644
--- a/API/Controllers/MetadataController.cs
+++ b/API/Controllers/MetadataController.cs
@@ -99,12 +99,12 @@ public class MetadataController : BaseApiController
/// String separated libraryIds or null for all publication status
///
[HttpGet("publication-status")]
- public async Task>> GetAllPublicationStatus(string? libraryIds)
+ public ActionResult> GetAllPublicationStatus(string? libraryIds)
{
var ids = libraryIds?.Split(",").Select(int.Parse).ToList();
- if (ids != null && ids.Count > 0)
+ if (ids is {Count: > 0})
{
- return Ok(await _unitOfWork.SeriesRepository.GetAllPublicationStatusesDtosForLibrariesAsync(ids));
+ return Ok(_unitOfWork.SeriesRepository.GetAllPublicationStatusesDtosForLibrariesAsync(ids));
}
return Ok(Enum.GetValues().Select(t => new PublicationStatusDto()
diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs
index eaa778121..a221f06c1 100644
--- a/API/Controllers/OPDSController.cs
+++ b/API/Controllers/OPDSController.cs
@@ -389,12 +389,8 @@ public class OpdsController : BaseApiController
var userParams = new UserParams()
{
PageNumber = pageNumber,
- PageSize = 20
};
- var results = await _unitOfWork.SeriesRepository.GetOnDeck(userId, 0, userParams, _filterDto);
- var listResults = results.DistinctBy(s => s.Name).Skip((userParams.PageNumber - 1) * userParams.PageSize)
- .Take(userParams.PageSize).ToList();
- var pagedList = new PagedList(listResults, listResults.Count, userParams.PageNumber, userParams.PageSize);
+ var pagedList = await _unitOfWork.SeriesRepository.GetOnDeck(userId, 0, userParams, _filterDto);
Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages);
diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs
index e2d067abd..7ced84d6a 100644
--- a/API/Controllers/ReaderController.cs
+++ b/API/Controllers/ReaderController.cs
@@ -11,6 +11,8 @@ using API.Entities;
using API.Extensions;
using API.Services;
using API.Services.Tasks;
+using API.SignalR;
+using Hangfire;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
@@ -25,23 +27,21 @@ namespace API.Controllers
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger _logger;
private readonly IReaderService _readerService;
- private readonly IDirectoryService _directoryService;
- private readonly ICleanupService _cleanupService;
private readonly IBookmarkService _bookmarkService;
+ private readonly IEventHub _eventHub;
///
public ReaderController(ICacheService cacheService,
IUnitOfWork unitOfWork, ILogger logger,
- IReaderService readerService, IDirectoryService directoryService,
- ICleanupService cleanupService, IBookmarkService bookmarkService)
+ IReaderService readerService, IBookmarkService bookmarkService,
+ IEventHub eventHub)
{
_cacheService = cacheService;
_unitOfWork = unitOfWork;
_logger = logger;
_readerService = readerService;
- _directoryService = directoryService;
- _cleanupService = cleanupService;
_bookmarkService = bookmarkService;
+ _eventHub = eventHub;
}
///
@@ -73,6 +73,41 @@ namespace API.Controllers
}
}
+ ///
+ /// Returns an image for a given bookmark series. Side effect: This will cache the bookmark images for reading.
+ ///
+ ///
+ /// Api key for the user the bookmarks are on
+ ///
+ /// We must use api key as bookmarks could be leaked to other users via the API
+ ///
+ [HttpGet("bookmark-image")]
+ public async Task GetBookmarkImage(int seriesId, string apiKey, int page)
+ {
+ if (page < 0) page = 0;
+ var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
+ var totalPages = await _cacheService.CacheBookmarkForSeries(userId, seriesId);
+ if (page > totalPages)
+ {
+ page = totalPages;
+ }
+
+ try
+ {
+ var path = _cacheService.GetCachedBookmarkPagePath(seriesId, page);
+ if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}");
+ var format = Path.GetExtension(path).Replace(".", "");
+
+ Response.AddCacheHeader(path, TimeSpan.FromMinutes(10).Seconds);
+ return PhysicalFile(path, "image/" + format, Path.GetFileName(path));
+ }
+ catch (Exception)
+ {
+ _cacheService.CleanupBookmarks(new []{ seriesId });
+ throw;
+ }
+ }
+
///
/// Returns various information about a Chapter. Side effect: This will cache the chapter images for reading.
///
@@ -81,6 +116,7 @@ namespace API.Controllers
[HttpGet("chapter-info")]
public async Task> GetChapterInfo(int chapterId)
{
+ if (chapterId <= 0) return null; // This can happen occasionally from UI, we should just ignore
var chapter = await _cacheService.Ensure(chapterId);
if (chapter == null) return BadRequest("Could not find Chapter");
@@ -104,6 +140,28 @@ namespace API.Controllers
});
}
+ ///
+ /// Returns various information about all bookmark files for a Series. Side effect: This will cache the bookmark images for reading.
+ ///
+ /// Series Id for all bookmarks
+ ///
+ [HttpGet("bookmark-info")]
+ public async Task> GetBookmarkInfo(int seriesId)
+ {
+ var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
+ var totalPages = await _cacheService.CacheBookmarkForSeries(user.Id, seriesId);
+ var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.None);
+
+ return Ok(new BookmarkInfoDto()
+ {
+ SeriesName = series.Name,
+ SeriesFormat = series.Format,
+ SeriesId = series.Id,
+ LibraryId = series.LibraryId,
+ Pages = totalPages,
+ });
+ }
+
[HttpPost("mark-read")]
public async Task MarkRead(MarkReadDto markReadDto)
@@ -111,13 +169,9 @@ namespace API.Controllers
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
await _readerService.MarkSeriesAsRead(user, markReadDto.SeriesId);
- if (await _unitOfWork.CommitAsync())
- {
- return Ok();
- }
+ if (!await _unitOfWork.CommitAsync()) return BadRequest("There was an issue saving progress");
-
- return BadRequest("There was an issue saving progress");
+ return Ok();
}
@@ -132,13 +186,9 @@ namespace API.Controllers
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Progress);
await _readerService.MarkSeriesAsUnread(user, markReadDto.SeriesId);
- if (await _unitOfWork.CommitAsync())
- {
- return Ok();
- }
+ if (!await _unitOfWork.CommitAsync()) return BadRequest("There was an issue saving progress");
-
- return BadRequest("There was an issue saving progress");
+ return Ok();
}
///
@@ -514,6 +564,7 @@ namespace API.Controllers
if (await _bookmarkService.BookmarkPage(user, bookmarkDto, path))
{
+ BackgroundJob.Enqueue(() => _cacheService.CleanupBookmarkCache(bookmarkDto.SeriesId));
return Ok();
}
@@ -533,6 +584,7 @@ namespace API.Controllers
if (await _bookmarkService.RemoveBookmarkPage(user, bookmarkDto))
{
+ BackgroundJob.Enqueue(() => _cacheService.CleanupBookmarkCache(bookmarkDto.SeriesId));
return Ok();
}
diff --git a/API/Controllers/ReadingListController.cs b/API/Controllers/ReadingListController.cs
index 83cdc1a04..1b72b20d2 100644
--- a/API/Controllers/ReadingListController.cs
+++ b/API/Controllers/ReadingListController.cs
@@ -7,6 +7,7 @@ using API.DTOs.ReadingLists;
using API.Entities;
using API.Extensions;
using API.Helpers;
+using API.SignalR;
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers
@@ -14,11 +15,13 @@ namespace API.Controllers
public class ReadingListController : BaseApiController
{
private readonly IUnitOfWork _unitOfWork;
+ private readonly IEventHub _eventHub;
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
- public ReadingListController(IUnitOfWork unitOfWork)
+ public ReadingListController(IUnitOfWork unitOfWork, IEventHub eventHub)
{
_unitOfWork = unitOfWork;
+ _eventHub = eventHub;
}
///
@@ -208,12 +211,8 @@ namespace API.Controllers
{
return BadRequest("A list of this name already exists");
}
- user.ReadingLists.Add(new ReadingList()
- {
- Promoted = false,
- Title = dto.Title,
- Summary = string.Empty
- });
+
+ user.ReadingLists.Add(DbFactory.ReadingList(dto.Title, string.Empty, false));
if (!_unitOfWork.HasChanges()) return BadRequest("There was a problem creating list");
@@ -233,9 +232,12 @@ namespace API.Controllers
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId);
if (readingList == null) return BadRequest("List does not exist");
+
+
if (!string.IsNullOrEmpty(dto.Title))
{
readingList.Title = dto.Title; // Should I check if this is unique?
+ readingList.NormalizedTitle = Parser.Parser.Normalize(readingList.Title);
}
if (!string.IsNullOrEmpty(dto.Title))
{
@@ -244,6 +246,19 @@ namespace API.Controllers
readingList.Promoted = dto.Promoted;
+ readingList.CoverImageLocked = dto.CoverImageLocked;
+
+ if (!dto.CoverImageLocked)
+ {
+ readingList.CoverImageLocked = false;
+ readingList.CoverImage = string.Empty;
+ await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
+ MessageFactory.CoverUpdateEvent(readingList.Id, MessageFactoryEntityTypes.ReadingList), false);
+ _unitOfWork.ReadingListRepository.Update(readingList);
+ }
+
+
+
_unitOfWork.ReadingListRepository.Update(readingList);
if (await _unitOfWork.CommitAsync())
@@ -455,14 +470,7 @@ namespace API.Controllers
foreach (var chapter in chaptersForSeries)
{
if (existingChapterExists.Contains(chapter.Id)) continue;
-
- readingList.Items.Add(new ReadingListItem()
- {
- Order = index,
- ChapterId = chapter.Id,
- SeriesId = seriesId,
- VolumeId = chapter.VolumeId
- });
+ readingList.Items.Add(DbFactory.ReadingListItem(index, seriesId, chapter.VolumeId, chapter.Id));
index += 1;
}
diff --git a/API/Controllers/RecommendedController.cs b/API/Controllers/RecommendedController.cs
new file mode 100644
index 000000000..acd200b97
--- /dev/null
+++ b/API/Controllers/RecommendedController.cs
@@ -0,0 +1,90 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using API.Data;
+using API.DTOs;
+using API.Extensions;
+using API.Helpers;
+using Microsoft.AspNetCore.Mvc;
+
+namespace API.Controllers;
+
+public class RecommendedController : BaseApiController
+{
+ private readonly IUnitOfWork _unitOfWork;
+
+ public RecommendedController(IUnitOfWork unitOfWork)
+ {
+ _unitOfWork = unitOfWork;
+ }
+
+
+ ///
+ /// Quick Reads are series that are less than 2K pages in total.
+ ///
+ /// Library to restrict series to
+ ///
+ [HttpGet("quick-reads")]
+ public async Task>> GetQuickReads(int libraryId, [FromQuery] UserParams userParams)
+ {
+ var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
+
+ userParams ??= new UserParams();
+ var series = await _unitOfWork.SeriesRepository.GetQuickReads(user.Id, libraryId, userParams);
+
+ Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
+ return Ok(series);
+ }
+
+ ///
+ /// Highly Rated based on other users ratings. Will pull series with ratings > 4.0, weighted by count of other users.
+ ///
+ /// Library to restrict series to
+ ///
+ [HttpGet("highly-rated")]
+ public async Task>> GetHighlyRated(int libraryId, [FromQuery] UserParams userParams)
+ {
+ var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
+
+ userParams ??= new UserParams();
+ var series = await _unitOfWork.SeriesRepository.GetHighlyRated(user.Id, libraryId, userParams);
+ await _unitOfWork.SeriesRepository.AddSeriesModifiers(user.Id, series);
+ Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
+ return Ok(series);
+ }
+
+ ///
+ /// Chooses a random genre and shows series that are in that without reading progress
+ ///
+ /// Library to restrict series to
+ ///
+ [HttpGet("more-in")]
+ public async Task>> GetMoreIn(int libraryId, int genreId, [FromQuery] UserParams userParams)
+ {
+ var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
+
+ userParams ??= new UserParams();
+ var series = await _unitOfWork.SeriesRepository.GetMoreIn(user.Id, libraryId, genreId, userParams);
+ await _unitOfWork.SeriesRepository.AddSeriesModifiers(user.Id, series);
+
+ Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
+ return Ok(series);
+ }
+
+ ///
+ /// Series that are fully read by the user in no particular order
+ ///
+ /// Library to restrict series to
+ ///
+ [HttpGet("rediscover")]
+ public async Task>> GetRediscover(int libraryId, [FromQuery] UserParams userParams)
+ {
+ var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
+
+ userParams ??= new UserParams();
+ var series = await _unitOfWork.SeriesRepository.GetRediscover(user.Id, libraryId, userParams);
+
+ Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
+ return Ok(series);
+ }
+
+}
diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs
index 3aad34d99..34e90d818 100644
--- a/API/Controllers/SeriesController.cs
+++ b/API/Controllers/SeriesController.cs
@@ -6,8 +6,10 @@ using API.Data;
using API.Data.Repositories;
using API.DTOs;
using API.DTOs.Filtering;
+using API.DTOs.SeriesDetail;
using API.Entities;
using API.Entities.Enums;
+using API.Entities.Metadata;
using API.Extensions;
using API.Helpers;
using API.Services;
@@ -214,13 +216,6 @@ namespace API.Controllers
return Ok(await _unitOfWork.SeriesRepository.GetRecentlyUpdatedSeries(userId));
}
- [HttpPost("recently-added-chapters")]
- public async Task>> GetRecentlyAddedChaptersAlt()
- {
- var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
- return Ok(await _unitOfWork.SeriesRepository.GetRecentlyAddedChapters(userId));
- }
-
[HttpPost("all")]
public async Task>> GetAllSeries(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
{
@@ -248,12 +243,8 @@ namespace API.Controllers
[HttpPost("on-deck")]
public async Task>> GetOnDeck(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
{
- // NOTE: This has to be done manually like this due to the DistinctBy requirement
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
- var results = await _unitOfWork.SeriesRepository.GetOnDeck(userId, libraryId, userParams, filterDto);
-
- var listResults = results.DistinctBy(s => s.Name).Skip((userParams.PageNumber - 1) * userParams.PageSize).Take(userParams.PageSize).ToList();
- var pagedList = new PagedList(listResults, listResults.Count, userParams.PageNumber, userParams.PageSize);
+ var pagedList = await _unitOfWork.SeriesRepository.GetOnDeck(userId, libraryId, userParams, filterDto);
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, pagedList);
@@ -346,5 +337,105 @@ namespace API.Controllers
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return await _seriesService.GetSeriesDetail(seriesId, userId);
}
+
+ ///
+ /// Returns the series for the MangaFile id. If the user does not have access (shouldn't happen by the UI),
+ /// then null is returned
+ ///
+ ///
+ ///
+ [HttpGet("series-for-mangafile")]
+ public async Task> GetSeriesForMangaFile(int mangaFileId)
+ {
+ var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
+ return Ok(await _unitOfWork.SeriesRepository.GetSeriesForMangaFile(mangaFileId, userId));
+ }
+
+ ///
+ /// Returns the series for the Chapter id. If the user does not have access (shouldn't happen by the UI),
+ /// then null is returned
+ ///
+ ///
+ ///
+ [HttpGet("series-for-chapter")]
+ public async Task> GetSeriesForChapter(int chapterId)
+ {
+ var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
+ return Ok(await _unitOfWork.SeriesRepository.GetSeriesForChapter(chapterId, userId));
+ }
+
+ ///
+ /// Fetches the related series for a given series
+ ///
+ ///
+ /// Type of Relationship to pull back
+ ///
+ [HttpGet("related")]
+ public async Task>> GetRelatedSeries(int seriesId, RelationKind relation)
+ {
+ // Send back a custom DTO with each type or maybe sorted in some way
+ var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
+ return Ok(await _unitOfWork.SeriesRepository.GetSeriesForRelationKind(userId, seriesId, relation));
+ }
+
+ [HttpGet("all-related")]
+ public async Task> GetAllRelatedSeries(int seriesId)
+ {
+ // Send back a custom DTO with each type or maybe sorted in some way
+ var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
+ return Ok(await _unitOfWork.SeriesRepository.GetRelatedSeries(userId, seriesId));
+ }
+
+ [Authorize(Policy="RequireAdminRole")]
+ [HttpPost("update-related")]
+ public async Task UpdateRelatedSeries(UpdateRelatedSeriesDto dto)
+ {
+ var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, SeriesIncludes.Related);
+
+ 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);
+ UpdateRelationForKind(dto.Contains, series.Relations.Where(r => r.RelationKind == RelationKind.Contains).ToList(), series, RelationKind.Contains);
+ UpdateRelationForKind(dto.Others, series.Relations.Where(r => r.RelationKind == RelationKind.Other).ToList(), series, RelationKind.Other);
+ UpdateRelationForKind(dto.SideStories, series.Relations.Where(r => r.RelationKind == RelationKind.SideStory).ToList(), series, RelationKind.SideStory);
+ UpdateRelationForKind(dto.SpinOffs, series.Relations.Where(r => r.RelationKind == RelationKind.SpinOff).ToList(), series, RelationKind.SpinOff);
+ UpdateRelationForKind(dto.AlternativeSettings, series.Relations.Where(r => r.RelationKind == RelationKind.AlternativeSetting).ToList(), series, RelationKind.AlternativeSetting);
+ UpdateRelationForKind(dto.AlternativeVersions, series.Relations.Where(r => r.RelationKind == RelationKind.AlternativeVersion).ToList(), series, RelationKind.AlternativeVersion);
+ UpdateRelationForKind(dto.Doujinshis, series.Relations.Where(r => r.RelationKind == RelationKind.Doujinshi).ToList(), series, RelationKind.Doujinshi);
+ UpdateRelationForKind(dto.Prequels, series.Relations.Where(r => r.RelationKind == RelationKind.Prequel).ToList(), series, RelationKind.Prequel);
+ UpdateRelationForKind(dto.Sequels, series.Relations.Where(r => r.RelationKind == RelationKind.Sequel).ToList(), series, RelationKind.Sequel);
+
+ if (!_unitOfWork.HasChanges()) return Ok();
+ if (await _unitOfWork.CommitAsync()) return Ok();
+
+
+ return BadRequest("There was an issue updating relationships");
+ }
+
+ private void UpdateRelationForKind(IList dtoTargetSeriesIds, IEnumerable adaptations, Series series, RelationKind kind)
+ {
+ foreach (var adaptation in adaptations.Where(adaptation => !dtoTargetSeriesIds.Contains(adaptation.TargetSeriesId)))
+ {
+ // If the seriesId isn't in dto, it means we've removed or reclassified
+ series.Relations.Remove(adaptation);
+ }
+
+ // At this point, we only have things to add
+ foreach (var targetSeriesId in dtoTargetSeriesIds)
+ {
+ // This ensures we don't allow any duplicates to be added
+ if (series.Relations.SingleOrDefault(r =>
+ r.RelationKind == kind && r.TargetSeriesId == targetSeriesId) !=
+ null) continue;
+
+ series.Relations.Add(new SeriesRelation()
+ {
+ Series = series,
+ SeriesId = series.Id,
+ TargetSeriesId = targetSeriesId,
+ RelationKind = kind
+ });
+ _unitOfWork.SeriesRepository.Update(series);
+ }
+ }
}
}
diff --git a/API/Controllers/ThemeController.cs b/API/Controllers/ThemeController.cs
index f6775d2dc..bf68e8641 100644
--- a/API/Controllers/ThemeController.cs
+++ b/API/Controllers/ThemeController.cs
@@ -2,6 +2,7 @@
using System.Threading.Tasks;
using API.Data;
using API.DTOs.Theme;
+using API.Extensions;
using API.Services;
using API.Services.Tasks;
using Kavita.Common;
@@ -13,13 +14,13 @@ namespace API.Controllers;
public class ThemeController : BaseApiController
{
private readonly IUnitOfWork _unitOfWork;
- private readonly ISiteThemeService _siteThemeService;
+ private readonly IThemeService _themeService;
private readonly ITaskScheduler _taskScheduler;
- public ThemeController(IUnitOfWork unitOfWork, ISiteThemeService siteThemeService, ITaskScheduler taskScheduler)
+ public ThemeController(IUnitOfWork unitOfWork, IThemeService themeService, ITaskScheduler taskScheduler)
{
_unitOfWork = unitOfWork;
- _siteThemeService = siteThemeService;
+ _themeService = themeService;
_taskScheduler = taskScheduler;
}
@@ -39,9 +40,9 @@ public class ThemeController : BaseApiController
[Authorize("RequireAdminRole")]
[HttpPost("update-default")]
- public async Task UpdateDefault(UpdateDefaultSiteThemeDto dto)
+ public async Task UpdateDefault(UpdateDefaultThemeDto dto)
{
- await _siteThemeService.UpdateDefault(dto.ThemeId);
+ await _themeService.UpdateDefault(dto.ThemeId);
return Ok();
}
@@ -54,7 +55,7 @@ public class ThemeController : BaseApiController
{
try
{
- return Ok(await _siteThemeService.GetContent(themeId));
+ return Ok(await _themeService.GetContent(themeId));
}
catch (KavitaException ex)
{
diff --git a/API/Controllers/UploadController.cs b/API/Controllers/UploadController.cs
index 4d07d4225..ca84acc8b 100644
--- a/API/Controllers/UploadController.cs
+++ b/API/Controllers/UploadController.cs
@@ -5,6 +5,7 @@ using API.Data;
using API.DTOs.Uploads;
using API.Extensions;
using API.Services;
+using API.SignalR;
using Flurl.Http;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -24,16 +25,18 @@ namespace API.Controllers
private readonly ILogger _logger;
private readonly ITaskScheduler _taskScheduler;
private readonly IDirectoryService _directoryService;
+ private readonly IEventHub _eventHub;
///
public UploadController(IUnitOfWork unitOfWork, IImageService imageService, ILogger logger,
- ITaskScheduler taskScheduler, IDirectoryService directoryService)
+ ITaskScheduler taskScheduler, IDirectoryService directoryService, IEventHub eventHub)
{
_unitOfWork = unitOfWork;
_imageService = imageService;
_logger = logger;
_taskScheduler = taskScheduler;
_directoryService = directoryService;
+ _eventHub = eventHub;
}
///
@@ -99,6 +102,8 @@ namespace API.Controllers
if (_unitOfWork.HasChanges())
{
+ await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
+ MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series), false);
await _unitOfWork.CommitAsync();
return Ok();
}
@@ -145,6 +150,8 @@ namespace API.Controllers
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
+ await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
+ MessageFactory.CoverUpdateEvent(tag.Id, MessageFactoryEntityTypes.CollectionTag), false);
return Ok();
}
@@ -158,6 +165,53 @@ namespace API.Controllers
return BadRequest("Unable to save cover image to Collection Tag");
}
+ ///
+ /// Replaces reading list cover image and locks it with a base64 encoded image
+ ///
+ ///
+ ///
+ [Authorize(Policy = "RequireAdminRole")]
+ [RequestSizeLimit(8_000_000)]
+ [HttpPost("reading-list")]
+ public async Task UploadReadingListCoverImageFromUrl(UploadFileDto uploadFileDto)
+ {
+ // Check if Url is non empty, request the image and place in temp, then ask image service to handle it.
+ // See if we can do this all in memory without touching underlying system
+ if (string.IsNullOrEmpty(uploadFileDto.Url))
+ {
+ return BadRequest("You must pass a url to use");
+ }
+
+ try
+ {
+ var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetReadingListFormat(uploadFileDto.Id)}");
+ var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(uploadFileDto.Id);
+
+ if (!string.IsNullOrEmpty(filePath))
+ {
+ readingList.CoverImage = filePath;
+ readingList.CoverImageLocked = true;
+ _unitOfWork.ReadingListRepository.Update(readingList);
+ }
+
+ if (_unitOfWork.HasChanges())
+ {
+ await _unitOfWork.CommitAsync();
+ await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
+ MessageFactory.CoverUpdateEvent(readingList.Id, MessageFactoryEntityTypes.ReadingList), false);
+ return Ok();
+ }
+
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "There was an issue uploading cover image for Reading List {Id}", uploadFileDto.Id);
+ await _unitOfWork.RollbackAsync();
+ }
+
+ return BadRequest("Unable to save cover image to Reading List");
+ }
+
///
/// Replaces chapter cover image and locks it with a base64 encoded image. This will update the parent volume's cover image.
///
@@ -193,6 +247,10 @@ namespace API.Controllers
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
+ await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
+ MessageFactory.CoverUpdateEvent(chapter.VolumeId, MessageFactoryEntityTypes.Volume), false);
+ await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
+ MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter), false);
return Ok();
}
diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs
index 3fda79468..97dae76a4 100644
--- a/API/Controllers/UsersController.cs
+++ b/API/Controllers/UsersController.cs
@@ -82,11 +82,13 @@ namespace API.Controllers
existingPreferences.BookReaderMargin = preferencesDto.BookReaderMargin;
existingPreferences.BookReaderLineSpacing = preferencesDto.BookReaderLineSpacing;
existingPreferences.BookReaderFontFamily = preferencesDto.BookReaderFontFamily;
- existingPreferences.BookReaderDarkMode = preferencesDto.BookReaderDarkMode;
existingPreferences.BookReaderFontSize = preferencesDto.BookReaderFontSize;
existingPreferences.BookReaderTapToPaginate = preferencesDto.BookReaderTapToPaginate;
existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection;
preferencesDto.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
+ existingPreferences.BookThemeName = preferencesDto.BookReaderThemeName;
+ existingPreferences.PageLayoutMode = preferencesDto.BookReaderLayoutMode;
+ existingPreferences.BookReaderImmersiveMode = preferencesDto.BookReaderImmersiveMode;
existingPreferences.Theme = await _unitOfWork.SiteThemeRepository.GetThemeById(preferencesDto.Theme.Id);
// TODO: Remove this code - this overrides layout mode to be single until the mode is released
diff --git a/API/DTOs/Account/InviteUserDto.cs b/API/DTOs/Account/InviteUserDto.cs
index 42d4bdf8e..9d0d9416d 100644
--- a/API/DTOs/Account/InviteUserDto.cs
+++ b/API/DTOs/Account/InviteUserDto.cs
@@ -6,7 +6,7 @@ namespace API.DTOs.Account;
public class InviteUserDto
{
[Required]
- public string Email { get; init; }
+ public string Email { get; set; }
///
/// 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.
diff --git a/API/DTOs/Filtering/FilterDto.cs b/API/DTOs/Filtering/FilterDto.cs
index 1a8d9fc8b..892f9e6b9 100644
--- a/API/DTOs/Filtering/FilterDto.cs
+++ b/API/DTOs/Filtering/FilterDto.cs
@@ -99,6 +99,5 @@ namespace API.DTOs.Filtering
/// An optional name string to filter by. Empty string will ignore.
///
public string SeriesNameQuery { get; init; } = string.Empty;
-
}
}
diff --git a/API/DTOs/Filtering/SortField.cs b/API/DTOs/Filtering/SortField.cs
index 0e465f6aa..3d78494bd 100644
--- a/API/DTOs/Filtering/SortField.cs
+++ b/API/DTOs/Filtering/SortField.cs
@@ -5,4 +5,5 @@ public enum SortField
SortName = 1,
CreatedDate = 2,
LastModifiedDate = 3,
+ LastChapterAdded = 4
}
diff --git a/API/DTOs/MangaFileDto.cs b/API/DTOs/MangaFileDto.cs
index 786f85df7..c1220e28d 100644
--- a/API/DTOs/MangaFileDto.cs
+++ b/API/DTOs/MangaFileDto.cs
@@ -4,9 +4,10 @@ namespace API.DTOs
{
public class MangaFileDto
{
+ public int Id { get; init; }
public string FilePath { get; init; }
public int Pages { get; init; }
public MangaFormat Format { get; init; }
-
+
}
-}
\ No newline at end of file
+}
diff --git a/API/DTOs/Reader/BookmarkInfoDto.cs b/API/DTOs/Reader/BookmarkInfoDto.cs
new file mode 100644
index 000000000..a34eb81c2
--- /dev/null
+++ b/API/DTOs/Reader/BookmarkInfoDto.cs
@@ -0,0 +1,13 @@
+using API.Entities.Enums;
+
+namespace API.DTOs.Reader;
+
+public class BookmarkInfoDto
+{
+ public string SeriesName { get; set; }
+ public MangaFormat SeriesFormat { get; set; }
+ public int SeriesId { get; set; }
+ public int LibraryId { get; set; }
+ public LibraryType LibraryType { get; set; }
+ public int Pages { get; set; }
+}
diff --git a/API/DTOs/ReadingLists/ReadingListDto.cs b/API/DTOs/ReadingLists/ReadingListDto.cs
index e3837a2e3..3eb5ded79 100644
--- a/API/DTOs/ReadingLists/ReadingListDto.cs
+++ b/API/DTOs/ReadingLists/ReadingListDto.cs
@@ -9,5 +9,6 @@
/// Reading lists that are promoted are only done by admins
///
public bool Promoted { get; set; }
+ public bool CoverImageLocked { get; set; }
}
}
diff --git a/API/DTOs/ReadingLists/UpdateReadingListDto.cs b/API/DTOs/ReadingLists/UpdateReadingListDto.cs
index a9f6f0d59..5b8f69731 100644
--- a/API/DTOs/ReadingLists/UpdateReadingListDto.cs
+++ b/API/DTOs/ReadingLists/UpdateReadingListDto.cs
@@ -6,5 +6,6 @@
public string Title { get; set; }
public string Summary { get; set; }
public bool Promoted { get; set; }
+ public bool CoverImageLocked { get; set; }
}
}
diff --git a/API/DTOs/Search/SearchResultGroupDto.cs b/API/DTOs/Search/SearchResultGroupDto.cs
index b21209dca..0a1fac402 100644
--- a/API/DTOs/Search/SearchResultGroupDto.cs
+++ b/API/DTOs/Search/SearchResultGroupDto.cs
@@ -17,5 +17,8 @@ public class SearchResultGroupDto
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; }
+
}
diff --git a/API/DTOs/SeriesDetail/RelatedSeriesDto.cs b/API/DTOs/SeriesDetail/RelatedSeriesDto.cs
new file mode 100644
index 000000000..f3c3fd644
--- /dev/null
+++ b/API/DTOs/SeriesDetail/RelatedSeriesDto.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using API.Entities.Enums;
+
+namespace API.DTOs.SeriesDetail;
+
+public class RelatedSeriesDto
+{
+ ///
+ /// The parent relationship Series
+ ///
+ 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; }
+}
diff --git a/API/DTOs/SeriesDetailDto.cs b/API/DTOs/SeriesDetail/SeriesDetailDto.cs
similarity index 100%
rename from API/DTOs/SeriesDetailDto.cs
rename to API/DTOs/SeriesDetail/SeriesDetailDto.cs
diff --git a/API/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs b/API/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs
new file mode 100644
index 000000000..b39f91244
--- /dev/null
+++ b/API/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+
+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; }
+}
diff --git a/API/DTOs/SeriesDto.cs b/API/DTOs/SeriesDto.cs
index 5f76634ff..a5756ceca 100644
--- a/API/DTOs/SeriesDto.cs
+++ b/API/DTOs/SeriesDto.cs
@@ -22,6 +22,10 @@ namespace API.DTOs
///
public DateTime LatestReadDate { get; set; }
///
+ /// DateTime representing last time a chapter was added to the Series
+ ///
+ public DateTime LastChapterAdded { get; set; }
+ ///
/// Rating from logged in user. Calculated at API-time.
///
public int UserRating { get; set; }
diff --git a/API/DTOs/SeriesMetadataDto.cs b/API/DTOs/SeriesMetadataDto.cs
index d5ad24610..9a396f5d1 100644
--- a/API/DTOs/SeriesMetadataDto.cs
+++ b/API/DTOs/SeriesMetadataDto.cs
@@ -45,11 +45,11 @@ namespace API.DTOs
///
public string Language { get; set; } = string.Empty;
///
- /// Number in the TotalCount of issues
+ /// Max number of issues/volumes in the series (Max of Volume/Issue field in ComicInfo)
///
- public int Count { get; set; }
+ public int MaxCount { get; set; } = 0;
///
- /// Total number of issues for the series
+ /// Total number of issues/volumes for the series
///
public int TotalCount { get; set; }
///
@@ -69,16 +69,16 @@ namespace API.DTOs
public bool PublicationStatusLocked { get; set; }
public bool GenresLocked { get; set; }
public bool TagsLocked { get; set; }
- public bool WriterLocked { get; set; }
- public bool CharacterLocked { get; set; }
- public bool ColoristLocked { get; set; }
- public bool EditorLocked { get; set; }
- public bool InkerLocked { get; set; }
- public bool LettererLocked { get; set; }
- public bool PencillerLocked { get; set; }
- public bool PublisherLocked { get; set; }
- public bool TranslatorLocked { get; set; }
- public bool CoverArtistLocked { get; set; }
+ public bool WritersLocked { get; set; }
+ public bool CharactersLocked { get; set; }
+ public bool ColoristsLocked { get; set; }
+ public bool EditorsLocked { get; set; }
+ public bool InkersLocked { get; set; }
+ public bool LetterersLocked { get; set; }
+ public bool PencillersLocked { get; set; }
+ public bool PublishersLocked { get; set; }
+ public bool TranslatorsLocked { get; set; }
+ public bool CoverArtistsLocked { get; set; }
public int SeriesId { get; set; }
diff --git a/API/DTOs/Theme/SiteThemeDto.cs b/API/DTOs/Theme/SiteThemeDto.cs
index e8b0460f9..7c44a1cd0 100644
--- a/API/DTOs/Theme/SiteThemeDto.cs
+++ b/API/DTOs/Theme/SiteThemeDto.cs
@@ -4,6 +4,9 @@ using API.Services;
namespace API.DTOs.Theme;
+///
+/// Represents a set of css overrides the user can upload to Kavita and will load into webui
+///
public class SiteThemeDto
{
public int Id { get; set; }
diff --git a/API/DTOs/Theme/UpdateDefaultSiteThemeDto.cs b/API/DTOs/Theme/UpdateDefaultThemeDto.cs
similarity index 64%
rename from API/DTOs/Theme/UpdateDefaultSiteThemeDto.cs
rename to API/DTOs/Theme/UpdateDefaultThemeDto.cs
index d4bdb8e09..0f2b129f3 100644
--- a/API/DTOs/Theme/UpdateDefaultSiteThemeDto.cs
+++ b/API/DTOs/Theme/UpdateDefaultThemeDto.cs
@@ -1,6 +1,6 @@
namespace API.DTOs.Theme;
-public class UpdateDefaultSiteThemeDto
+public class UpdateDefaultThemeDto
{
public int ThemeId { get; set; }
}
diff --git a/API/DTOs/UserPreferencesDto.cs b/API/DTOs/UserPreferencesDto.cs
index 4bfcb2d77..4fc2f6904 100644
--- a/API/DTOs/UserPreferencesDto.cs
+++ b/API/DTOs/UserPreferencesDto.cs
@@ -1,4 +1,5 @@
-using API.Entities;
+using API.DTOs.Theme;
+using API.Entities;
using API.Entities.Enums;
namespace API.DTOs
@@ -74,5 +75,12 @@ namespace API.DTOs
///
/// Should default to Dark
public SiteTheme Theme { get; set; }
+ public string BookReaderThemeName { get; set; }
+ public BookPageLayoutMode BookReaderLayoutMode { get; set; }
+ ///
+ /// Book Reader Option: A flag that hides the menu-ing system behind a click on the screen. This should be used with tap to paginate, but the app doesn't enforce this.
+ ///
+ /// Defaults to false
+ public bool BookReaderImmersiveMode { get; set; } = false;
}
}
diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs
index 6822467a8..154cdf2fc 100644
--- a/API/Data/DataContext.cs
+++ b/API/Data/DataContext.cs
@@ -41,6 +41,7 @@ namespace API.Data
public DbSet Genre { get; set; }
public DbSet Tag { get; set; }
public DbSet SiteTheme { get; set; }
+ public DbSet SeriesRelation { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
@@ -59,10 +60,28 @@ namespace API.Data
.WithOne(u => u.Role)
.HasForeignKey(ur => ur.RoleId)
.IsRequired();
+
+ builder.Entity()
+ .HasOne(pt => pt.Series)
+ .WithMany(p => p.Relations)
+ .HasForeignKey(pt => pt.SeriesId)
+ .OnDelete(DeleteBehavior.ClientCascade);
+
+ builder.Entity()
+ .HasOne(pt => pt.TargetSeries)
+ .WithMany(t => t.RelationOf)
+ .HasForeignKey(pt => pt.TargetSeriesId);
+
+ builder.Entity()
+ .Property(b => b.BookThemeName)
+ .HasDefaultValue("Dark");
+ builder.Entity()
+ .Property(b => b.BackgroundColor)
+ .HasDefaultValue("#000000");
}
- void OnEntityTracked(object sender, EntityTrackedEventArgs e)
+ static void OnEntityTracked(object sender, EntityTrackedEventArgs e)
{
if (!e.FromQuery && e.Entry.State == EntityState.Added && e.Entry.Entity is IEntityDate entity)
{
@@ -72,7 +91,7 @@ namespace API.Data
}
- void OnEntityStateChanged(object sender, EntityStateChangedEventArgs e)
+ static void OnEntityStateChanged(object sender, EntityStateChangedEventArgs e)
{
if (e.NewState == EntityState.Modified && e.Entry.Entity is IEntityDate entity)
entity.LastModified = DateTime.Now;
diff --git a/API/Data/DbFactory.cs b/API/Data/DbFactory.cs
index 46ebfbf10..ad97958da 100644
--- a/API/Data/DbFactory.cs
+++ b/API/Data/DbFactory.cs
@@ -35,7 +35,7 @@ namespace API.Data
return new Volume()
{
Name = volumeNumber,
- Number = (int) Parser.Parser.MinimumNumberFromRange(volumeNumber),
+ Number = (int) Parser.Parser.MinNumberFromRange(volumeNumber),
Chapters = new List()
};
}
@@ -46,7 +46,7 @@ namespace API.Data
var specialTitle = specialTreatment ? info.Filename : info.Chapters;
return new Chapter()
{
- Number = specialTreatment ? "0" : Parser.Parser.MinimumNumberFromRange(info.Chapters) + string.Empty,
+ Number = specialTreatment ? "0" : Parser.Parser.MinNumberFromRange(info.Chapters) + string.Empty,
Range = specialTreatment ? info.Filename : info.Chapters,
Title = (specialTreatment && info.Format == MangaFormat.Epub)
? info.Title
@@ -82,6 +82,29 @@ namespace API.Data
};
}
+ public static ReadingList ReadingList(string title, string summary, bool promoted)
+ {
+ return new ReadingList()
+ {
+ NormalizedTitle = API.Parser.Parser.Normalize(title?.Trim()).ToUpper(),
+ Title = title?.Trim(),
+ Summary = summary?.Trim(),
+ Promoted = promoted,
+ Items = new List()
+ };
+ }
+
+ public static ReadingListItem ReadingListItem(int index, int seriesId, int volumeId, int chapterId)
+ {
+ return new ReadingListItem()
+ {
+ Order = index,
+ ChapterId = chapterId,
+ SeriesId = seriesId,
+ VolumeId = volumeId
+ };
+ }
+
public static Genre Genre(string name, bool external)
{
return new Genre()
diff --git a/API/Data/Metadata/ComicInfo.cs b/API/Data/Metadata/ComicInfo.cs
index 040d7f6b7..0c236fd58 100644
--- a/API/Data/Metadata/ComicInfo.cs
+++ b/API/Data/Metadata/ComicInfo.cs
@@ -14,6 +14,10 @@ namespace API.Data.Metadata
public string Summary { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Series { get; set; } = string.Empty;
+ ///
+ /// Localized Series name. Not standard.
+ ///
+ public string LocalizedSeries { get; set; } = string.Empty;
public string SeriesSort { get; set; } = string.Empty;
public string Number { get; set; } = string.Empty;
///
@@ -47,11 +51,11 @@ namespace API.Data.Metadata
///
public float UserRating { get; set; }
- public string AlternateSeries { get; set; } = string.Empty;
public string StoryArc { get; set; } = string.Empty;
public string SeriesGroup { get; set; } = string.Empty;
- public string AlternativeSeries { get; set; } = string.Empty;
- public string AlternativeNumber { get; set; } = string.Empty;
+ public string AlternateNumber { get; set; } = string.Empty;
+ public int AlternateCount { get; set; } = 0;
+ public string AlternateSeries { get; set; } = string.Empty;
///
/// This is Epub only: calibre:title_sort
@@ -94,6 +98,10 @@ namespace API.Data.Metadata
{
if (info == null) return;
+ info.Series = info.Series.Trim();
+ info.SeriesSort = info.SeriesSort.Trim();
+ info.LocalizedSeries = info.LocalizedSeries.Trim();
+
info.Writer = Parser.Parser.CleanAuthor(info.Writer);
info.Colorist = Parser.Parser.CleanAuthor(info.Colorist);
info.Editor = Parser.Parser.CleanAuthor(info.Editor);
diff --git a/API/Data/MigrateRemoveExtraThemes.cs b/API/Data/MigrateRemoveExtraThemes.cs
new file mode 100644
index 000000000..1c9a1e9b0
--- /dev/null
+++ b/API/Data/MigrateRemoveExtraThemes.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using API.Services.Tasks;
+
+namespace API.Data;
+
+///
+/// In v0.5.3, we removed Light and E-Ink themes. This migration will remove the themes from the DB and default anyone on
+/// null, E-Ink, or Light to Dark.
+///
+public static class MigrateRemoveExtraThemes
+{
+ public static async Task Migrate(IUnitOfWork unitOfWork, IThemeService themeService)
+ {
+ Console.WriteLine("Removing Dark and E-Ink themes");
+
+ var themes = (await unitOfWork.SiteThemeRepository.GetThemes()).ToList();
+
+ if (themes.FirstOrDefault(t => t.Name.Equals("Light")) == null)
+ {
+ Console.WriteLine("Done. Nothing to do");
+ return;
+ }
+
+ var darkTheme = themes.Single(t => t.Name.Equals("Dark"));
+ var lightTheme = themes.Single(t => t.Name.Equals("Light"));
+ var eInkTheme = themes.Single(t => t.Name.Equals("E-Ink"));
+
+
+
+ // Update default theme if it's not Dark or a custom theme
+ await themeService.UpdateDefault(darkTheme.Id);
+
+ // Update all users to Dark theme if they are on Light/E-Ink
+ foreach (var pref in await unitOfWork.UserRepository.GetAllPreferencesByThemeAsync(lightTheme.Id))
+ {
+ pref.Theme = darkTheme;
+ }
+ foreach (var pref in await unitOfWork.UserRepository.GetAllPreferencesByThemeAsync(eInkTheme.Id))
+ {
+ pref.Theme = darkTheme;
+ }
+
+ // Remove Light/E-Ink themes
+ foreach (var siteTheme in themes.Where(t => t.Name.Equals("Light") || t.Name.Equals("E-Ink")))
+ {
+ unitOfWork.SiteThemeRepository.Remove(siteTheme);
+ }
+ // Commit and call it a day
+ await unitOfWork.CommitAsync();
+
+ Console.WriteLine("Completed removing Dark and E-Ink themes");
+ }
+
+}
diff --git a/API/Data/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.Designer.cs b/API/Data/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.Designer.cs
new file mode 100644
index 000000000..27d16bfde
--- /dev/null
+++ b/API/Data/Migrations/20220410230540_SeriesLastChapterAddedAndReadingListNormalization.Designer.cs
@@ -0,0 +1,1469 @@
+//
+using System;
+using API.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace API.Data.Migrations
+{
+ [DbContext(typeof(DataContext))]
+ [Migration("20220410230540_SeriesLastChapterAddedAndReadingListNormalization")]
+ partial class SeriesLastChapterAddedAndReadingListNormalization
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "6.0.3");
+
+ modelBuilder.Entity("API.Entities.AppRole", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex");
+
+ b.ToTable("AspNetRoles", (string)null);
+ });
+
+ modelBuilder.Entity("API.Entities.AppUser", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("ApiKey")
+ .HasColumnType("TEXT");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("TEXT");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastActive")
+ .HasColumnType("TEXT");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedEmail")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedUserName")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("PasswordHash")
+ .HasColumnType("TEXT");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("TEXT");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("INTEGER");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("TEXT");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("UserName")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedEmail")
+ .HasDatabaseName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasDatabaseName("UserNameIndex");
+
+ b.ToTable("AspNetUsers", (string)null);
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserBookmark", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ChapterId")
+ .HasColumnType("INTEGER");
+
+ b.Property("FileName")
+ .HasColumnType("TEXT");
+
+ b.Property("Page")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("VolumeId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.ToTable("AppUserBookmark");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("AutoCloseMenu")
+ .HasColumnType("INTEGER");
+
+ b.Property("BackgroundColor")
+ .HasColumnType("TEXT");
+
+ b.Property("BookReaderDarkMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderFontFamily")
+ .HasColumnType("TEXT");
+
+ b.Property("BookReaderFontSize")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderLineSpacing")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderMargin")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderReadingDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderTapToPaginate")
+ .HasColumnType("INTEGER");
+
+ b.Property("LayoutMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("PageSplitOption")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReaderMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReadingDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("ScalingOption")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowScreenHints")
+ .HasColumnType("INTEGER");
+
+ b.Property("ThemeId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId")
+ .IsUnique();
+
+ b.HasIndex("ThemeId");
+
+ b.ToTable("AppUserPreferences");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserProgress", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookScrollId")
+ .HasColumnType("TEXT");
+
+ b.Property("ChapterId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("PagesRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("VolumeId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.HasIndex("SeriesId");
+
+ b.ToTable("AppUserProgresses");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserRating", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Rating")
+ .HasColumnType("INTEGER");
+
+ b.Property("Review")
+ .HasColumnType("TEXT");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.HasIndex("SeriesId");
+
+ b.ToTable("AppUserRating");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("RoleId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("API.Entities.Chapter", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property("Count")
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("CoverImageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("IsSpecial")
+ .HasColumnType("INTEGER");
+
+ b.Property("Language")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Number")
+ .HasColumnType("TEXT");
+
+ b.Property("Pages")
+ .HasColumnType("INTEGER");
+
+ b.Property("Range")
+ .HasColumnType("TEXT");
+
+ b.Property("ReleaseDate")
+ .HasColumnType("TEXT");
+
+ b.Property("Summary")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.Property("TitleName")
+ .HasColumnType("TEXT");
+
+ b.Property("TotalCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("VolumeId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("VolumeId");
+
+ b.ToTable("Chapter");
+ });
+
+ modelBuilder.Entity("API.Entities.CollectionTag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("CoverImageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("NormalizedTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("Promoted")
+ .HasColumnType("INTEGER");
+
+ b.Property("RowVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property("Summary")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Id", "Promoted")
+ .IsUnique();
+
+ b.ToTable("CollectionTag");
+ });
+
+ modelBuilder.Entity("API.Entities.FolderPath", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("LastScanned")
+ .HasColumnType("TEXT");
+
+ b.Property("LibraryId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Path")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LibraryId");
+
+ b.ToTable("FolderPath");
+ });
+
+ modelBuilder.Entity("API.Entities.Genre", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ExternalTag")
+ .HasColumnType("INTEGER");
+
+ b.Property("NormalizedTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedTitle", "ExternalTag")
+ .IsUnique();
+
+ b.ToTable("Genre");
+ });
+
+ modelBuilder.Entity("API.Entities.Library", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LastScanned")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("Library");
+ });
+
+ modelBuilder.Entity("API.Entities.MangaFile", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ChapterId")
+ .HasColumnType("INTEGER");
+
+ b.Property("FilePath")
+ .HasColumnType("TEXT");
+
+ b.Property("Format")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Pages")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChapterId");
+
+ b.ToTable("MangaFile");
+ });
+
+ modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property("AgeRatingLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("CharacterLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("ColoristLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Count")
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverArtistLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("EditorLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("GenresLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("InkerLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Language")
+ .HasColumnType("TEXT");
+
+ b.Property("LanguageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("LettererLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("PencillerLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("PublicationStatus")
+ .HasColumnType("INTEGER");
+
+ b.Property("PublicationStatusLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("PublisherLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReleaseYear")
+ .HasColumnType("INTEGER");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Summary")
+ .HasColumnType("TEXT");
+
+ b.Property("SummaryLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("TagsLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("TranslatorLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("WriterLocked")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("SeriesId")
+ .IsUnique();
+
+ b.HasIndex("Id", "SeriesId")
+ .IsUnique();
+
+ b.ToTable("SeriesMetadata");
+ });
+
+ modelBuilder.Entity("API.Entities.Person", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedName")
+ .HasColumnType("TEXT");
+
+ b.Property("Role")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("Person");
+ });
+
+ modelBuilder.Entity("API.Entities.ReadingList", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("CoverImageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("Promoted")
+ .HasColumnType("INTEGER");
+
+ b.Property("Summary")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.ToTable("ReadingList");
+ });
+
+ modelBuilder.Entity("API.Entities.ReadingListItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ChapterId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReadingListId")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("VolumeId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChapterId");
+
+ b.HasIndex("ReadingListId");
+
+ b.HasIndex("SeriesId");
+
+ b.HasIndex("VolumeId");
+
+ b.ToTable("ReadingListItem");
+ });
+
+ modelBuilder.Entity("API.Entities.Series", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("CoverImageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("Format")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastChapterAdded")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LibraryId")
+ .HasColumnType("INTEGER");
+
+ b.Property("LocalizedName")
+ .HasColumnType("TEXT");
+
+ b.Property("LocalizedNameLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("NameLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("NormalizedName")
+ .HasColumnType("TEXT");
+
+ b.Property("OriginalName")
+ .HasColumnType("TEXT");
+
+ b.Property("Pages")
+ .HasColumnType("INTEGER");
+
+ b.Property("SortName")
+ .HasColumnType("TEXT");
+
+ b.Property("SortNameLocked")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LibraryId");
+
+ b.HasIndex("Name", "NormalizedName", "LocalizedName", "LibraryId", "Format")
+ .IsUnique();
+
+ b.ToTable("Series");
+ });
+
+ modelBuilder.Entity("API.Entities.ServerSetting", b =>
+ {
+ b.Property("Key")
+ .HasColumnType("INTEGER");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Key");
+
+ b.ToTable("ServerSetting");
+ });
+
+ modelBuilder.Entity("API.Entities.SiteTheme", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("FileName")
+ .HasColumnType("TEXT");
+
+ b.Property("IsDefault")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedName")
+ .HasColumnType("TEXT");
+
+ b.Property("Provider")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("SiteTheme");
+ });
+
+ modelBuilder.Entity("API.Entities.Tag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ExternalTag")
+ .HasColumnType("INTEGER");
+
+ b.Property("NormalizedTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedTitle", "ExternalTag")
+ .IsUnique();
+
+ b.ToTable("Tag");
+ });
+
+ modelBuilder.Entity("API.Entities.Volume", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("Number")
+ .HasColumnType("INTEGER");
+
+ b.Property("Pages")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("SeriesId");
+
+ b.ToTable("Volume");
+ });
+
+ modelBuilder.Entity("AppUserLibrary", b =>
+ {
+ b.Property("AppUsersId")
+ .HasColumnType("INTEGER");
+
+ b.Property("LibrariesId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("AppUsersId", "LibrariesId");
+
+ b.HasIndex("LibrariesId");
+
+ b.ToTable("AppUserLibrary");
+ });
+
+ modelBuilder.Entity("ChapterGenre", b =>
+ {
+ b.Property("ChaptersId")
+ .HasColumnType("INTEGER");
+
+ b.Property("GenresId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ChaptersId", "GenresId");
+
+ b.HasIndex("GenresId");
+
+ b.ToTable("ChapterGenre");
+ });
+
+ modelBuilder.Entity("ChapterPerson", b =>
+ {
+ b.Property("ChapterMetadatasId")
+ .HasColumnType("INTEGER");
+
+ b.Property("PeopleId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ChapterMetadatasId", "PeopleId");
+
+ b.HasIndex("PeopleId");
+
+ b.ToTable("ChapterPerson");
+ });
+
+ modelBuilder.Entity("ChapterTag", b =>
+ {
+ b.Property("ChaptersId")
+ .HasColumnType("INTEGER");
+
+ b.Property("TagsId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ChaptersId", "TagsId");
+
+ b.HasIndex("TagsId");
+
+ b.ToTable("ChapterTag");
+ });
+
+ modelBuilder.Entity("CollectionTagSeriesMetadata", b =>
+ {
+ b.Property("CollectionTagsId")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesMetadatasId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("CollectionTagsId", "SeriesMetadatasId");
+
+ b.HasIndex("SeriesMetadatasId");
+
+ b.ToTable("CollectionTagSeriesMetadata");
+ });
+
+ modelBuilder.Entity("GenreSeriesMetadata", b =>
+ {
+ b.Property("GenresId")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesMetadatasId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("GenresId", "SeriesMetadatasId");
+
+ b.HasIndex("SeriesMetadatasId");
+
+ b.ToTable("GenreSeriesMetadata");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ClaimType")
+ .HasColumnType("TEXT");
+
+ b.Property("ClaimValue")
+ .HasColumnType("TEXT");
+
+ b.Property("RoleId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ClaimType")
+ .HasColumnType("TEXT");
+
+ b.Property("ClaimValue")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasColumnType("TEXT");
+
+ b.Property("ProviderKey")
+ .HasColumnType("TEXT");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("LoginProvider")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property