diff --git a/API.Benchmark/TestBenchmark.cs b/API.Benchmark/TestBenchmark.cs index 0b4880690..3b08bbcdf 100644 --- a/API.Benchmark/TestBenchmark.cs +++ b/API.Benchmark/TestBenchmark.cs @@ -25,7 +25,7 @@ public class TestBenchmark { list.Add(new VolumeDto() { - Number = random.Next(10) > 5 ? 1 : 0, + MinNumber = random.Next(10) > 5 ? 1 : 0, Chapters = GenerateChapters() }); } @@ -49,7 +49,7 @@ public class TestBenchmark private static void SortSpecialChapters(IEnumerable volumes) { - foreach (var v in volumes.Where(vDto => vDto.Number == 0)) + foreach (var v in volumes.Where(vDto => vDto.MinNumber == 0)) { v.Chapters = v.Chapters.OrderByNatural(x => x.Range).ToList(); } diff --git a/API.Tests/Extensions/SeriesExtensionsTests.cs b/API.Tests/Extensions/SeriesExtensionsTests.cs index 6a706e892..c14de4439 100644 --- a/API.Tests/Extensions/SeriesExtensionsTests.cs +++ b/API.Tests/Extensions/SeriesExtensionsTests.cs @@ -192,7 +192,7 @@ public class SeriesExtensionsTests .Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("0") .WithIsSpecial(false) .WithCoverImage("Volume 1") @@ -229,7 +229,7 @@ public class SeriesExtensionsTests .Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("0") .WithIsSpecial(false) .WithCoverImage("Volume 1") @@ -266,14 +266,14 @@ public class SeriesExtensionsTests .Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("0") .WithIsSpecial(false) .WithCoverImage("Volume 1") .Build()) .Build()) .WithVolume(new VolumeBuilder("137") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("0") .WithIsSpecial(false) .WithCoverImage("Volume 137") @@ -306,7 +306,7 @@ public class SeriesExtensionsTests .Build()) .Build()) .WithVolume(new VolumeBuilder("4") - .WithNumber(4) + .WithMinNumber(4) .WithChapter(new ChapterBuilder("0") .WithIsSpecial(false) .WithCoverImage("Volume 4") diff --git a/API.Tests/Extensions/VolumeListExtensionsTests.cs b/API.Tests/Extensions/VolumeListExtensionsTests.cs index 2db82eeda..e64267896 100644 --- a/API.Tests/Extensions/VolumeListExtensionsTests.cs +++ b/API.Tests/Extensions/VolumeListExtensionsTests.cs @@ -27,7 +27,7 @@ public class VolumeListExtensionsTests .Build(), }; - Assert.Equal(volumes[0].Number, volumes.GetCoverImage(MangaFormat.Archive).Number); + Assert.Equal(volumes[0].MinNumber, volumes.GetCoverImage(MangaFormat.Archive).MinNumber); } [Fact] diff --git a/API.Tests/Services/BookmarkServiceTests.cs b/API.Tests/Services/BookmarkServiceTests.cs index 25c7bd5de..6a82f457d 100644 --- a/API.Tests/Services/BookmarkServiceTests.cs +++ b/API.Tests/Services/BookmarkServiceTests.cs @@ -180,7 +180,7 @@ Substitute.For()); var series = new SeriesBuilder("Test") .WithFormat(MangaFormat.Epub) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("0") .Build()) .Build()) @@ -246,7 +246,7 @@ Substitute.For()); var series = new SeriesBuilder("Test") .WithFormat(MangaFormat.Epub) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1") .Build()) .Build()) @@ -322,7 +322,7 @@ Substitute.For()); var series = new SeriesBuilder("Test") .WithFormat(MangaFormat.Epub) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1") .Build()) .Build()) @@ -375,7 +375,7 @@ Substitute.For()); var series = new SeriesBuilder("Test") .WithFormat(MangaFormat.Epub) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1") .Build()) .Build()) @@ -428,7 +428,7 @@ Substitute.For()); var series = new SeriesBuilder("Test") .WithFormat(MangaFormat.Epub) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1") .Build()) .Build()) diff --git a/API.Tests/Services/CleanupServiceTests.cs b/API.Tests/Services/CleanupServiceTests.cs index 0002b4e6a..d1aa96fb2 100644 --- a/API.Tests/Services/CleanupServiceTests.cs +++ b/API.Tests/Services/CleanupServiceTests.cs @@ -395,7 +395,7 @@ public class CleanupServiceTests : AbstractDbTest var series = new SeriesBuilder("Test") .WithFormat(MangaFormat.Epub) .WithVolume(new VolumeBuilder("0") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(c) .Build()) .Build(); diff --git a/API.Tests/Services/ReaderServiceTests.cs b/API.Tests/Services/ReaderServiceTests.cs index 2b86e6646..322ae4d9d 100644 --- a/API.Tests/Services/ReaderServiceTests.cs +++ b/API.Tests/Services/ReaderServiceTests.cs @@ -136,7 +136,7 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("0") .WithPages(1) .Build()) @@ -166,7 +166,7 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("0") .WithPages(1) .Build()) @@ -205,7 +205,7 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("0") .WithPages(1) .Build()) @@ -260,7 +260,7 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("0") .WithPages(1) .Build()) @@ -299,7 +299,7 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("0") .WithPages(1) .Build()) @@ -347,19 +347,19 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithNumber(2) + .WithMinNumber(2) .WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("22").Build()) .Build()) .WithVolume(new VolumeBuilder("3") - .WithNumber(3) + .WithMinNumber(3) .WithChapter(new ChapterBuilder("31").Build()) .WithChapter(new ChapterBuilder("32").Build()) .Build()) @@ -382,7 +382,7 @@ public class ReaderServiceTests Assert.Equal("2", actualChapter.Range); } - //[Fact] + [Fact] public async Task GetNextChapterIdAsync_ShouldGetNextVolume_WhenUsingRanges() { // V1 -> V2 @@ -390,15 +390,13 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1-2") - .WithNumber(1) - .WithChapter(new ChapterBuilder("1").Build()) - .WithChapter(new ChapterBuilder("2").Build()) + .WithMinNumber(1) + .WithChapter(new ChapterBuilder("0").Build()) .Build()) .WithVolume(new VolumeBuilder("3-4") - .WithNumber(2) - .WithChapter(new ChapterBuilder("21").Build()) - .WithChapter(new ChapterBuilder("22").Build()) + .WithMinNumber(2) + .WithChapter(new ChapterBuilder("1").Build()) .Build()) .Build(); series.Library = new LibraryBuilder("Test Lib", LibraryType.Manga).Build(); @@ -414,7 +412,8 @@ public class ReaderServiceTests var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 1, 1); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); - Assert.Equal("3-4", actualChapter.Range); + Assert.Equal("3-4", actualChapter.Volume.Name); + Assert.Equal("1", actualChapter.Range); } [Fact] @@ -467,19 +466,19 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithNumber(2) + .WithMinNumber(2) .WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("22").Build()) .Build()) .WithVolume(new VolumeBuilder("3") - .WithNumber(3) + .WithMinNumber(3) .WithChapter(new ChapterBuilder("31").Build()) .WithChapter(new ChapterBuilder("32").Build()) .Build()) @@ -508,19 +507,19 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("1.5") - .WithNumber(2) + .WithMinNumber(2) .WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("22").Build()) .Build()) .WithVolume(new VolumeBuilder("3") - .WithNumber(3) + .WithMinNumber(3) .WithChapter(new ChapterBuilder("31").Build()) .WithChapter(new ChapterBuilder("32").Build()) .Build()) @@ -550,13 +549,13 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("22").Build()) .Build()) @@ -585,18 +584,18 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("66").Build()) .WithChapter(new ChapterBuilder("67").Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithNumber(2) + .WithMinNumber(2) .WithChapter(new ChapterBuilder("0").Build()) .Build()) .Build(); @@ -627,13 +626,13 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) .Build()) @@ -659,7 +658,7 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) @@ -685,7 +684,7 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) @@ -712,14 +711,14 @@ public class ReaderServiceTests // // var series = new SeriesBuilder("Test") // .WithVolume(new VolumeBuilder("0") - // .WithNumber(0) + // .WithMinNumber(0) // .WithChapter(new ChapterBuilder("1").Build()) // .WithChapter(new ChapterBuilder("2").Build()) // .WithChapter(new ChapterBuilder("0").WithIsSpecial(true).Build()) // .Build()) // // .WithVolume(new VolumeBuilder("1") - // .WithNumber(1) + // .WithMinNumber(1) // .WithChapter(new ChapterBuilder("2").Build()) // .Build()) // .Build(); @@ -746,13 +745,13 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) .Build()) @@ -782,7 +781,7 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) @@ -813,13 +812,13 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("0").Build()) .Build()) .Build(); @@ -846,12 +845,12 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) .Build()) @@ -881,12 +880,12 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("12").Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithNumber(2) + .WithMinNumber(2) .WithChapter(new ChapterBuilder("12").Build()) .Build()) .Build(); @@ -907,7 +906,7 @@ public class ReaderServiceTests var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 1, 1); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter, ChapterIncludes.Volumes); - Assert.Equal(2, actualChapter.Volume.Number); + Assert.Equal(2, actualChapter.Volume.MinNumber); } #endregion @@ -922,19 +921,19 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithNumber(2) + .WithMinNumber(2) .WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("22").Build()) .Build()) .WithVolume(new VolumeBuilder("3") - .WithNumber(3) + .WithMinNumber(3) .WithChapter(new ChapterBuilder("31").Build()) .WithChapter(new ChapterBuilder("32").Build()) .Build()) @@ -965,19 +964,19 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("1.5") - .WithNumber(2) + .WithMinNumber(2) .WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("22").Build()) .Build()) .WithVolume(new VolumeBuilder("3") - .WithNumber(3) + .WithMinNumber(3) .WithChapter(new ChapterBuilder("31").Build()) .WithChapter(new ChapterBuilder("32").Build()) .Build()) @@ -1089,13 +1088,13 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) .Build()) @@ -1127,7 +1126,7 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) @@ -1157,7 +1156,7 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("0").Build()) .Build()) .Build(); @@ -1186,13 +1185,13 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("0").Build()) .Build()) .Build(); @@ -1221,20 +1220,20 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("5").Build()) .WithChapter(new ChapterBuilder("6").Build()) .WithChapter(new ChapterBuilder("7").Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("2").WithIsSpecial(true).Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithNumber(2) + .WithMinNumber(2) .WithChapter(new ChapterBuilder("3").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("4").WithIsSpecial(true).Build()) .Build()) @@ -1269,7 +1268,7 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) @@ -1299,12 +1298,12 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) .Build()) @@ -1337,12 +1336,12 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("0") - .WithNumber(0) + .WithMinNumber(0) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .Build()) .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("22").Build()) .Build()) @@ -1375,12 +1374,12 @@ public class ReaderServiceTests var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("12").Build()) .Build()) .WithVolume(new VolumeBuilder("2") - .WithNumber(2) + .WithMinNumber(2) .WithChapter(new ChapterBuilder("12").Build()) .Build()) .Build(); @@ -1396,7 +1395,7 @@ public class ReaderServiceTests var nextChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 2, 1); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter, ChapterIncludes.Volumes); - Assert.Equal(1, actualChapter.Volume.Number); + Assert.Equal(1, actualChapter.Volume.MinNumber); } #endregion diff --git a/API.Tests/Services/ReadingListServiceTests.cs b/API.Tests/Services/ReadingListServiceTests.cs index c6d9675d0..23de53674 100644 --- a/API.Tests/Services/ReadingListServiceTests.cs +++ b/API.Tests/Services/ReadingListServiceTests.cs @@ -759,7 +759,7 @@ public class ReadingListServiceTests var fablesSeries = new SeriesBuilder("Fables").Build(); fablesSeries.Volumes.Add( new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithName("2002") .WithChapter(new ChapterBuilder("1").Build()) .Build() @@ -937,7 +937,7 @@ public class ReadingListServiceTests var fables2Series = new SeriesBuilder("Fables: The Last Castle").Build(); fablesSeries.Volumes.Add(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithName("2002") .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) @@ -945,7 +945,7 @@ public class ReadingListServiceTests .Build() ); fables2Series.Volumes.Add(new VolumeBuilder("1") - .WithNumber(1) + .WithMinNumber(1) .WithName("2003") .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) @@ -980,13 +980,13 @@ public class ReadingListServiceTests var fables2Series = new SeriesBuilder("Fablesa: The Last Castle").Build(); fablesSeries.Volumes.Add(new VolumeBuilder("2002") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("3").Build()) .Build()); fables2Series.Volumes.Add(new VolumeBuilder("2003") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("3").Build()) @@ -1036,7 +1036,7 @@ public class ReadingListServiceTests // Mock up our series var fablesSeries = new SeriesBuilder("Fables") .WithVolume(new VolumeBuilder("2002") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("3").Build()) @@ -1045,7 +1045,7 @@ public class ReadingListServiceTests var fables2Series = new SeriesBuilder("Fables: The Last Castle") .WithVolume(new VolumeBuilder("2003") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("3").Build()) @@ -1094,13 +1094,13 @@ public class ReadingListServiceTests var fables2Series = new SeriesBuilder("Fables: The Last Castle").Build(); fablesSeries.Volumes.Add(new VolumeBuilder("2002") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("3").Build()) .Build()); fables2Series.Volumes.Add(new VolumeBuilder("2003") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("3").Build()) @@ -1153,13 +1153,13 @@ public class ReadingListServiceTests var fables2Series = new SeriesBuilder("Fables: The Last Castle").Build(); fablesSeries.Volumes.Add(new VolumeBuilder("2002") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("3").Build()) .Build()); fables2Series.Volumes.Add(new VolumeBuilder("2003") - .WithNumber(1) + .WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("3").Build()) diff --git a/API/API.csproj b/API/API.csproj index a799fb3ce..806467f6f 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -95,7 +95,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/API/Controllers/DownloadController.cs b/API/Controllers/DownloadController.cs index fa21bd12d..05fd7ea27 100644 --- a/API/Controllers/DownloadController.cs +++ b/API/Controllers/DownloadController.cs @@ -103,7 +103,7 @@ public class DownloadController : BaseApiController var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId); try { - return await DownloadFiles(files, $"download_{User.GetUsername()}_v{volumeId}", $"{series!.Name} - Volume {volume.Number}.zip"); + return await DownloadFiles(files, $"download_{User.GetUsername()}_v{volumeId}", $"{series!.Name} - Volume {volume.Name}.zip"); } catch (KavitaException ex) { diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index 3f8a11282..f112905ea 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -1109,7 +1109,7 @@ public class OpdsController : BaseApiController title += $" - {volume.Name}"; } } - else if (volume.Number != 0) + else if (volume.MinNumber != 0) { title = $"{series.Name} - Volume {volume.Name} - {await _seriesService.FormatChapterTitle(userId, chapter, libraryType)}"; } diff --git a/API/DTOs/SeriesDetail/NextExpectedChapterDto.cs b/API/DTOs/SeriesDetail/NextExpectedChapterDto.cs index df4cc1a07..0f1a8eb4b 100644 --- a/API/DTOs/SeriesDetail/NextExpectedChapterDto.cs +++ b/API/DTOs/SeriesDetail/NextExpectedChapterDto.cs @@ -5,7 +5,7 @@ namespace API.DTOs.SeriesDetail; public class NextExpectedChapterDto { public float ChapterNumber { get; set; } - public int VolumeNumber { get; set; } + public float VolumeNumber { get; set; } /// /// Null if not applicable /// diff --git a/API/DTOs/VolumeDto.cs b/API/DTOs/VolumeDto.cs index 8b1d49a7a..1e9a308dc 100644 --- a/API/DTOs/VolumeDto.cs +++ b/API/DTOs/VolumeDto.cs @@ -9,9 +9,10 @@ namespace API.DTOs; public class VolumeDto : IHasReadTimeEstimate { public int Id { get; set; } - /// - public int Number { get; set; } - + /// + public float MinNumber { get; set; } + /// + public float MaxNumber { get; set; } /// public string Name { get; set; } = default!; public int Pages { get; set; } diff --git a/API/Data/ManualMigrations/MigrateVolumeNumber.cs b/API/Data/ManualMigrations/MigrateVolumeNumber.cs new file mode 100644 index 000000000..4df4e29af --- /dev/null +++ b/API/Data/ManualMigrations/MigrateVolumeNumber.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading.Tasks; +using API.Entities.Enums; +using API.Services.Tasks.Scanner.Parser; +using Kavita.Common.EnvironmentInfo; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace API.Data.ManualMigrations; + +/// +/// Introduced in v0.7.14, this migrates the existing Volume Name -> Volume Min/Max Number +/// +public static class MigrateVolumeNumber +{ + public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger logger) + { + logger.LogCritical( + "Running MigrateVolumeNumber migration - Please be patient, this may take some time. This is not an error"); + if (await dataContext.Volume.AnyAsync(v => v.MaxNumber > 0)) + { + logger.LogCritical( + "Running MigrateVolumeNumber migration - Completed. This is not an error"); + return; + } + + // Get all volumes + foreach (var volume in dataContext.Volume) + { + volume.MinNumber = Parser.MinNumberFromRange(volume.Name); + volume.MaxNumber = Parser.MaxNumberFromRange(volume.Name); + } + + await dataContext.SaveChangesAsync(); + logger.LogCritical( + "Running MigrateVolumeNumber migration - Completed. This is not an error"); + } +} diff --git a/API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.Designer.cs b/API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.Designer.cs new file mode 100644 index 000000000..730b40ec0 --- /dev/null +++ b/API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.Designer.cs @@ -0,0 +1,2793 @@ +// +using System; +using API.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace API.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20240128153433_VolumeMinMaxNumbers")] + partial class VolumeMinMaxNumbers + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("AgeRestriction") + .HasColumnType("INTEGER"); + + b.Property("AgeRestrictionIncludeUnknowns") + .HasColumnType("INTEGER"); + + b.Property("AniListAccessToken") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("ConfirmationToken") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LastActiveUtc") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Page") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserBookmark"); + }); + + modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("IsProvided") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("SmartFilterId") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(4); + + b.Property("Visible") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SmartFilterId"); + + b.HasIndex("Visible"); + + b.ToTable("AppUserDashboardStream"); + }); + + modelBuilder.Entity("API.Entities.AppUserExternalSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Host") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserExternalSource"); + }); + + modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserOnDeckRemoval"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AutoCloseMenu") + .HasColumnType("INTEGER"); + + b.Property("BackgroundColor") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("#000000"); + + b.Property("BlurUnreadSummaries") + .HasColumnType("INTEGER"); + + b.Property("BookReaderFontFamily") + .HasColumnType("TEXT"); + + b.Property("BookReaderFontSize") + .HasColumnType("INTEGER"); + + b.Property("BookReaderImmersiveMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLayoutMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLineSpacing") + .HasColumnType("INTEGER"); + + b.Property("BookReaderMargin") + .HasColumnType("INTEGER"); + + b.Property("BookReaderReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("BookReaderTapToPaginate") + .HasColumnType("INTEGER"); + + b.Property("BookReaderWritingStyle") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("BookThemeName") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("Dark"); + + b.Property("CollapseSeriesRelationships") + .HasColumnType("INTEGER"); + + b.Property("EmulateBook") + .HasColumnType("INTEGER"); + + b.Property("GlobalPageLayoutMode") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("LayoutMode") + .HasColumnType("INTEGER"); + + b.Property("Locale") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("en"); + + b.Property("NoTransitions") + .HasColumnType("INTEGER"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("PromptForDownloadSize") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("ShareReviews") + .HasColumnType("INTEGER"); + + b.Property("ShowScreenHints") + .HasColumnType("INTEGER"); + + b.Property("SwipeToPaginate") + .HasColumnType("INTEGER"); + + b.Property("ThemeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.HasIndex("ThemeId"); + + b.ToTable("AppUserPreferences"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("HasBeenRated") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSourceId") + .HasColumnType("INTEGER"); + + b.Property("IsProvided") + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("SmartFilterId") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(5); + + b.Property("Visible") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SmartFilterId"); + + b.HasIndex("Visible"); + + b.ToTable("AppUserSideNavStream"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Filter") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserSmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PageNumber") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserTableOfContent"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AlternateCount") + .HasColumnType("INTEGER"); + + b.Property("AlternateNumber") + .HasColumnType("TEXT"); + + b.Property("AlternateSeries") + .HasColumnType("TEXT"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("ISBN") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SeriesGroup") + .HasColumnType("TEXT"); + + b.Property("StoryArc") + .HasColumnType("TEXT"); + + b.Property("StoryArcNumber") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TitleName") + .HasColumnType("TEXT"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("API.Entities.CollectionTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Promoted") + .IsUnique(); + + b.ToTable("CollectionTag"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastUsed") + .HasColumnType("TEXT"); + + b.Property("LastUsedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Platform") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("Device"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowScrobbling") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderWatching") + .HasColumnType("INTEGER"); + + b.Property("IncludeInDashboard") + .HasColumnType("INTEGER"); + + b.Property("IncludeInRecommended") + .HasColumnType("INTEGER"); + + b.Property("IncludeInSearch") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("ManageCollections") + .HasColumnType("INTEGER"); + + b.Property("ManageReadingLists") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Pattern") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("LibraryExcludePattern"); + }); + + modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FileTypeGroup") + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("LibraryFileTypeGroup"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Bytes") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastFileAnalysis") + .HasColumnType("TEXT"); + + b.Property("LastFileAnalysisUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.MediaError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MediaError"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AverageScore") + .HasColumnType("INTEGER"); + + b.Property("FavoriteCount") + .HasColumnType("INTEGER"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("ProviderUrl") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ExternalRating"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("CoverUrl") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("ExternalRecommendation"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("BodyJustText") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("RawBody") + .HasColumnType("TEXT"); + + b.Property("Score") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("SiteUrl") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("TotalVotes") + .HasColumnType("INTEGER"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ExternalReview"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AverageExternalRating") + .HasColumnType("INTEGER"); + + b.Property("GoogleBooksId") + .HasColumnType("TEXT"); + + b.Property("LastUpdatedUtc") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.ToTable("ExternalSeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AgeRatingLocked") + .HasColumnType("INTEGER"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("CoverArtistLocked") + .HasColumnType("INTEGER"); + + b.Property("EditorLocked") + .HasColumnType("INTEGER"); + + b.Property("GenresLocked") + .HasColumnType("INTEGER"); + + b.Property("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxCount") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatusLocked") + .HasColumnType("INTEGER"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYear") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYearLocked") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.HasIndex("Id", "SeriesId") + .IsUnique(); + + b.ToTable("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RelationKind") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("TargetSeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.HasIndex("TargetSeriesId"); + + b.ToTable("SeriesRelation"); + }); + + modelBuilder.Entity("API.Entities.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EndingMonth") + .HasColumnType("INTEGER"); + + b.Property("EndingYear") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("StartingMonth") + .HasColumnType("INTEGER"); + + b.Property("StartingYear") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("ReadingList"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ReadingListId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.HasIndex("ReadingListId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("VolumeId"); + + b.ToTable("ReadingListItem"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId1") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ScrobbleEventId1"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleError"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterNumber") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("IsProcessed") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("ProcessDateUtc") + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("ReviewBody") + .HasColumnType("TEXT"); + + b.Property("ReviewTitle") + .HasColumnType("TEXT"); + + b.Property("ScrobbleEventType") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeNumber") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("LibraryId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleEvent"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleHold"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderPath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastChapterAdded") + .HasColumnType("TEXT"); + + b.Property("LastChapterAddedUtc") + .HasColumnType("TEXT"); + + b.Property("LastFolderScanned") + .HasColumnType("TEXT"); + + b.Property("LastFolderScannedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("LocalizedNameLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedLocalizedName") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("SortNameLocked") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.ServerSetting", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("ServerSetting"); + }); + + modelBuilder.Entity("API.Entities.ServerStatistics", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterCount") + .HasColumnType("INTEGER"); + + b.Property("FileCount") + .HasColumnType("INTEGER"); + + b.Property("GenreCount") + .HasColumnType("INTEGER"); + + b.Property("PersonCount") + .HasColumnType("INTEGER"); + + b.Property("SeriesCount") + .HasColumnType("INTEGER"); + + b.Property("TagCount") + .HasColumnType("INTEGER"); + + b.Property("UserCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeCount") + .HasColumnType("INTEGER"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ServerStatistics"); + }); + + modelBuilder.Entity("API.Entities.SiteTheme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SiteTheme"); + }); + + modelBuilder.Entity("API.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MaxNumber") + .HasColumnType("REAL"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinNumber") + .HasColumnType("REAL"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "GenresId"); + + b.HasIndex("GenresId"); + + b.ToTable("ChapterGenre"); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.Property("ChapterMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterMetadatasId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.ToTable("ChapterPerson"); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("ChapterTag"); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.Property("CollectionTagsId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionTagsId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("CollectionTagSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => + { + b.Property("ExternalRatingsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalRatingExternalSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => + { + b.Property("ExternalRecommendationsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalRecommendationExternalSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => + { + b.Property("ExternalReviewsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalReviewExternalSeriesMetadata"); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("GenresId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("GenreSeriesMetadata"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("PersonSeriesMetadata", b => + { + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("PeopleId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("PersonSeriesMetadata"); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("SeriesMetadatasId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("SeriesMetadataTag"); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("DashboardStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserExternalSource", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ExternalSources") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.SiteTheme", "Theme") + .WithMany() + .HasForeignKey("ThemeId"); + + b.Navigation("AppUser"); + + b.Navigation("Theme"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", null) + .WithMany("UserProgress") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Progress") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SideNavStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SmartFilters") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("TableOfContents") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Devices") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("LibraryExcludePatterns") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("LibraryFileTypes") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Files") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("ExternalSeriesMetadata") + .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("Metadata") + .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Relations") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "TargetSeries") + .WithMany("RelationOf") + .HasForeignKey("TargetSeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + + b.Navigation("TargetSeries"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ReadingLists") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.ReadingList", "ReadingList") + .WithMany("Items") + .HasForeignKey("ReadingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Volume", "Volume") + .WithMany() + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("ReadingList"); + + b.Navigation("Series"); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") + .WithMany() + .HasForeignKey("ScrobbleEventId1"); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ScrobbleEvent"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", "Library") + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Library"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ScrobbleHolds") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany("WantToRead") + .HasForeignKey("AppUserId"); + + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterPerson", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChapterMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.HasOne("API.Entities.CollectionTag", null) + .WithMany() + .HasForeignKey("CollectionTagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalRating", null) + .WithMany() + .HasForeignKey("ExternalRatingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) + .WithMany() + .HasForeignKey("ExternalRecommendationsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalReview", null) + .WithMany() + .HasForeignKey("ExternalReviewsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("PersonSeriesMetadata", b => + { + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Bookmarks"); + + b.Navigation("DashboardStreams"); + + b.Navigation("Devices"); + + b.Navigation("ExternalSources"); + + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("ReadingLists"); + + b.Navigation("ScrobbleHolds"); + + b.Navigation("SideNavStreams"); + + b.Navigation("SmartFilters"); + + b.Navigation("TableOfContents"); + + b.Navigation("UserPreferences"); + + b.Navigation("UserRoles"); + + b.Navigation("WantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("Files"); + + b.Navigation("UserProgress"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("LibraryExcludePatterns"); + + b.Navigation("LibraryFileTypes"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("ExternalSeriesMetadata"); + + b.Navigation("Metadata"); + + b.Navigation("Progress"); + + b.Navigation("Ratings"); + + b.Navigation("RelationOf"); + + b.Navigation("Relations"); + + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Chapters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.cs b/API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.cs new file mode 100644 index 000000000..491fd057f --- /dev/null +++ b/API/Data/Migrations/20240128153433_VolumeMinMaxNumbers.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + /// + public partial class VolumeMinMaxNumbers : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "MaxNumber", + table: "Volume", + type: "REAL", + nullable: false, + defaultValue: 0f); + + migrationBuilder.AddColumn( + name: "MinNumber", + table: "Volume", + type: "REAL", + nullable: false, + defaultValue: 0f); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "MaxNumber", + table: "Volume"); + + migrationBuilder.DropColumn( + name: "MinNumber", + table: "Volume"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 925f3f73a..6ccc3759f 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -1781,9 +1781,15 @@ namespace API.Data.Migrations b.Property("MaxHoursToRead") .HasColumnType("INTEGER"); + b.Property("MaxNumber") + .HasColumnType("REAL"); + b.Property("MinHoursToRead") .HasColumnType("INTEGER"); + b.Property("MinNumber") + .HasColumnType("REAL"); + b.Property("Name") .HasColumnType("TEXT"); diff --git a/API/Data/Misc/RecentlyAddedSeries.cs b/API/Data/Misc/RecentlyAddedSeries.cs index d5cfece45..1ea5b1d3e 100644 --- a/API/Data/Misc/RecentlyAddedSeries.cs +++ b/API/Data/Misc/RecentlyAddedSeries.cs @@ -18,6 +18,6 @@ public class RecentlyAddedSeries public string? ChapterRange { get; init; } public string? ChapterTitle { get; init; } public bool IsSpecial { get; init; } - public int VolumeNumber { get; init; } + public float VolumeNumber { get; init; } public AgeRating AgeRating { get; init; } } diff --git a/API/Data/Repositories/AppUserProgressRepository.cs b/API/Data/Repositories/AppUserProgressRepository.cs index 2d1033621..2878938a5 100644 --- a/API/Data/Repositories/AppUserProgressRepository.cs +++ b/API/Data/Repositories/AppUserProgressRepository.cs @@ -32,7 +32,7 @@ public interface IAppUserProgressRepository Task GetUserProgressDtoAsync(int chapterId, int userId); Task AnyUserProgressForSeriesAsync(int seriesId, int userId); Task GetHighestFullyReadChapterForSeries(int seriesId, int userId); - Task GetHighestFullyReadVolumeForSeries(int seriesId, int userId); + Task GetHighestFullyReadVolumeForSeries(int seriesId, int userId); Task GetLatestProgressForSeries(int seriesId, int userId); Task GetFirstProgressForSeries(int seriesId, int userId); Task UpdateAllProgressThatAreMoreThanChapterPages(); @@ -172,14 +172,14 @@ public class AppUserProgressRepository : IAppUserProgressRepository return list.Count == 0 ? 0 : list.DefaultIfEmpty().Where(d => d != null).Max(d => (int) Math.Floor(Parser.MaxNumberFromRange(d))); } - public async Task GetHighestFullyReadVolumeForSeries(int seriesId, int userId) + public async Task GetHighestFullyReadVolumeForSeries(int seriesId, int userId) { var list = await _context.AppUserProgresses .Join(_context.Chapter, appUserProgresses => appUserProgresses.ChapterId, chapter => chapter.Id, (appUserProgresses, chapter) => new {appUserProgresses, chapter}) .Where(p => p.appUserProgresses.SeriesId == seriesId && p.appUserProgresses.AppUserId == userId && p.appUserProgresses.PagesRead >= p.chapter.Pages) - .Select(p => p.chapter.Volume.Number) + .Select(p => p.chapter.Volume.MaxNumber) .ToListAsync(); return list.Count == 0 ? 0 : list.DefaultIfEmpty().Max(); } diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index d4af1802a..78588a607 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -1836,7 +1836,7 @@ public class SeriesRepository : ISeriesRepository ChapterNumber = c.Number, ChapterRange = c.Range, IsSpecial = c.IsSpecial, - VolumeNumber = c.Volume.Number, + VolumeNumber = c.Volume.MinNumber, ChapterTitle = c.Title, AgeRating = c.Volume.Series.Metadata.AgeRating }) diff --git a/API/Data/Repositories/VolumeRepository.cs b/API/Data/Repositories/VolumeRepository.cs index ccd909117..f6bb9ff1a 100644 --- a/API/Data/Repositories/VolumeRepository.cs +++ b/API/Data/Repositories/VolumeRepository.cs @@ -152,7 +152,7 @@ public class VolumeRepository : IVolumeRepository .Include(vol => vol.Chapters) .ThenInclude(c => c.Files) .AsSplitQuery() - .OrderBy(vol => vol.Number) + .OrderBy(vol => vol.MinNumber) .ToListAsync(); } @@ -185,7 +185,7 @@ public class VolumeRepository : IVolumeRepository .ThenInclude(c => c.People) .Include(vol => vol.Chapters) .ThenInclude(c => c.Tags) - .OrderBy(volume => volume.Number) + .OrderBy(volume => volume.MinNumber) .ProjectTo(_mapper.ConfigurationProvider) .AsNoTracking() .AsSplitQuery() @@ -215,7 +215,7 @@ public class VolumeRepository : IVolumeRepository private static void SortSpecialChapters(IEnumerable volumes) { - foreach (var v in volumes.Where(vDto => vDto.Number == 0)) + foreach (var v in volumes.Where(vDto => vDto.MinNumber == 0)) { v.Chapters = v.Chapters.OrderByNatural(x => x.Range).ToList(); } diff --git a/API/Entities/Scrobble/ScrobbleEvent.cs b/API/Entities/Scrobble/ScrobbleEvent.cs index 2fd36eef3..7f1ac4444 100644 --- a/API/Entities/Scrobble/ScrobbleEvent.cs +++ b/API/Entities/Scrobble/ScrobbleEvent.cs @@ -36,7 +36,7 @@ public class ScrobbleEvent : IEntityDate /// /// Depends on the ScrobbleEvent if filled in /// - public int? VolumeNumber { get; set; } + public int? VolumeNumber { get; set; } // TODO: Migrate this to float /// /// Has this event been processed and pushed to Provider /// diff --git a/API/Entities/Volume.cs b/API/Entities/Volume.cs index 2316e6a03..2a2e4b29a 100644 --- a/API/Entities/Volume.cs +++ b/API/Entities/Volume.cs @@ -15,7 +15,17 @@ public class Volume : IEntityDate, IHasReadTimeEstimate /// /// The minimum number in the Name field in Int form /// - public required int Number { get; set; } + /// Removed in v0.7.13.8, this was an int and we need the ability to have 0.5 volumes render on the UI + [Obsolete("Use MinNumber and MaxNumber instead")] + public int Number { get; set; } + /// + /// The minimum number in the Name field + /// + public required float MinNumber { get; set; } + /// + /// The maximum number in the Name field (same as Minimum if Name isn't a range) + /// + public required float MaxNumber { get; set; } public IList Chapters { get; set; } = null!; public DateTime Created { get; set; } public DateTime LastModified { get; set; } diff --git a/API/Extensions/SeriesExtensions.cs b/API/Extensions/SeriesExtensions.cs index ba8bcc83e..5db96a30c 100644 --- a/API/Extensions/SeriesExtensions.cs +++ b/API/Extensions/SeriesExtensions.cs @@ -18,8 +18,8 @@ public static class SeriesExtensions /// This is under the assumption that the Volume already has a Cover Image calculated and set public static string? GetCoverImage(this Series series) { - var volumes = (series.Volumes ?? new List()) - .OrderBy(v => v.Number, ChapterSortComparer.Default) + var volumes = (series.Volumes ?? []) + .OrderBy(v => v.MinNumber, ChapterSortComparer.Default) .ToList(); var firstVolume = volumes.GetCoverImage(series.Format); if (firstVolume == null) return null; @@ -34,20 +34,20 @@ public static class SeriesExtensions } // just volumes - if (volumes.TrueForAll(v => $"{v.Number}" != Parser.DefaultVolume)) + if (volumes.TrueForAll(v => $"{v.MinNumber}" != Parser.DefaultVolume)) { return firstVolume.CoverImage; } // If we have loose leaf chapters // if loose leaf chapters AND volumes, just return first volume - if (volumes.Count >= 1 && $"{volumes[0].Number}" != Parser.DefaultVolume) + if (volumes.Count >= 1 && $"{volumes[0].MinNumber}" != Parser.DefaultVolume) { - var looseLeafChapters = volumes.Where(v => $"{v.Number}" == Parser.DefaultVolume) + var looseLeafChapters = volumes.Where(v => $"{v.MinNumber}" == Parser.DefaultVolume) .SelectMany(c => c.Chapters.Where(c => !c.IsSpecial)) .OrderBy(c => c.Number.AsDouble(), ChapterSortComparerZeroFirst.Default) .ToList(); - if (looseLeafChapters.Count > 0 && (1.0f * volumes[0].Number) > looseLeafChapters[0].Number.AsFloat()) + if (looseLeafChapters.Count > 0 && (1.0f * volumes[0].MinNumber) > looseLeafChapters[0].Number.AsFloat()) { return looseLeafChapters[0].CoverImage; } @@ -55,7 +55,7 @@ public static class SeriesExtensions } var firstLooseLeafChapter = volumes - .Where(v => $"{v.Number}" == Parser.DefaultVolume) + .Where(v => $"{v.MinNumber}" == Parser.DefaultVolume) .SelectMany(v => v.Chapters) .OrderBy(c => c.Number.AsDouble(), ChapterSortComparerZeroFirst.Default) .FirstOrDefault(c => !c.IsSpecial); diff --git a/API/Extensions/VolumeListExtensions.cs b/API/Extensions/VolumeListExtensions.cs index 51dc5cf8c..f1ef051b1 100644 --- a/API/Extensions/VolumeListExtensions.cs +++ b/API/Extensions/VolumeListExtensions.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using API.Entities; using API.Entities.Enums; +using API.Services.Tasks.Scanner.Parser; namespace API.Extensions; #nullable enable @@ -22,16 +23,16 @@ public static class VolumeListExtensions if (seriesFormat == MangaFormat.Epub || seriesFormat == MangaFormat.Pdf) { - return volumes.MinBy(x => x.Number); + return volumes.MinBy(x => x.MinNumber); } - if (volumes.Any(x => x.Number != 0)) + if (volumes.Any(x => x.MinNumber != 0f)) // TODO: Refactor this so we can avoid a magic number { - return volumes.OrderBy(x => x.Number).FirstOrDefault(x => x.Number != 0); + return volumes.OrderBy(x => x.MinNumber).FirstOrDefault(x => x.MinNumber != 0); } // We only have 1 volume of chapters, we need to be cautious if there are specials, as we don't want to order them first - return volumes.MinBy(x => x.Number); + return volumes.MinBy(x => x.MinNumber); } } diff --git a/API/Helpers/Builders/VolumeBuilder.cs b/API/Helpers/Builders/VolumeBuilder.cs index aabde6ce2..158a84bfa 100644 --- a/API/Helpers/Builders/VolumeBuilder.cs +++ b/API/Helpers/Builders/VolumeBuilder.cs @@ -15,8 +15,8 @@ public class VolumeBuilder : IEntityBuilder _volume = new Volume() { Name = volumeNumber, - // TODO / BUG: Try to use float based Number which will allow Epub's with < 1 volumes to show in series detail - Number = (int) Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(volumeNumber), + MinNumber = Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(volumeNumber), + MaxNumber = Services.Tasks.Scanner.Parser.Parser.MaxNumberFromRange(volumeNumber), Chapters = new List() }; } @@ -27,9 +27,25 @@ public class VolumeBuilder : IEntityBuilder return this; } - public VolumeBuilder WithNumber(int number) + public VolumeBuilder WithNumber(float number) { - _volume.Number = number; + _volume.MinNumber = number; + if (_volume.MaxNumber < number) + { + _volume.MaxNumber = number; + } + return this; + } + + public VolumeBuilder WithMinNumber(float number) + { + _volume.MinNumber = number; + return this; + } + + public VolumeBuilder WithMaxNumber(float number) + { + _volume.MaxNumber = number; return this; } diff --git a/API/Services/Plus/ScrobblingService.cs b/API/Services/Plus/ScrobblingService.cs index a598ca079..e6f1530b3 100644 --- a/API/Services/Plus/ScrobblingService.cs +++ b/API/Services/Plus/ScrobblingService.cs @@ -298,7 +298,7 @@ public class ScrobblingService : IScrobblingService var prevVol = $"{existingEvt.VolumeNumber}"; existingEvt.VolumeNumber = - await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId); + (int) await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId); existingEvt.ChapterNumber = await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(seriesId, userId); _unitOfWork.ScrobbleRepository.Update(existingEvt); @@ -319,7 +319,7 @@ public class ScrobblingService : IScrobblingService MalId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite), AppUserId = userId, VolumeNumber = - await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId), + (int) await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId), ChapterNumber = await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(seriesId, userId), Format = LibraryTypeHelper.GetFormat(series.Library.Type), @@ -660,7 +660,7 @@ public class ScrobblingService : IScrobblingService foreach (var readEvt in readEvents) { readEvt.VolumeNumber = - await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(readEvt.SeriesId, + (int) await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(readEvt.SeriesId, readEvt.AppUser.Id); readEvt.ChapterNumber = await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(readEvt.SeriesId, diff --git a/API/Services/ReaderService.cs b/API/Services/ReaderService.cs index be46fc90b..29cf756c5 100644 --- a/API/Services/ReaderService.cs +++ b/API/Services/ReaderService.cs @@ -365,18 +365,17 @@ public class ReaderService : IReaderService var currentVolume = volumes.Single(v => v.Id == volumeId); var currentChapter = currentVolume.Chapters.Single(c => c.Id == currentChapterId); - if (currentVolume.Number == 0) + if (currentVolume.MinNumber == 0) { // Handle specials by sorting on their Filename aka Range var chapterId = GetNextChapterId(currentVolume.Chapters.OrderByNatural(x => x.Range), currentChapter.Range, dto => dto.Range); if (chapterId > 0) return chapterId; } - var currentVolumeNumber = currentVolume.Name.AsFloat(); var next = false; foreach (var volume in volumes) { - var volumeNumbersMatch = Math.Abs(volume.Name.AsFloat() - currentVolumeNumber) < 0.00001f; + var volumeNumbersMatch = volume.Name == currentVolume.Name; if (volumeNumbersMatch && volume.Chapters.Count > 1) { // Handle Chapters within current Volume @@ -420,9 +419,9 @@ public class ReaderService : IReaderService else if (firstChapter.Number.AsDouble() == 0) return firstChapter.Id; // If on last volume AND there are no specials left, then let's return -1 - var anySpecials = volumes.Where(v => $"{v.Number}" == Parser.DefaultVolume) + var anySpecials = volumes.Where(v => $"{v.MinNumber}" == Parser.DefaultVolume) .SelectMany(v => v.Chapters.Where(c => c.IsSpecial)).Any(); - if (currentVolume.Number != 0 && !anySpecials) + if (currentVolume.MinNumber != 0 && !anySpecials) { return -1; } @@ -434,10 +433,10 @@ public class ReaderService : IReaderService // This has an added problem that it will loop up to the beginning always // Should I change this to Max number? volumes.LastOrDefault()?.Number -> volumes.Max(v => v.Number) - if (currentVolume.Number != 0 && currentVolume.Number == volumes.LastOrDefault()?.Number && volumes.Count > 1) + if (currentVolume.MinNumber != 0 && currentVolume.MinNumber == volumes.LastOrDefault()?.MinNumber && volumes.Count > 1) { var chapterVolume = volumes.FirstOrDefault(); - if (chapterVolume?.Number != 0) return -1; + if (chapterVolume?.MinNumber != 0) return -1; // This is my attempt at fixing a bug where we loop around to the beginning, but I just can't seem to figure it out // var orderedVolumes = volumes.OrderBy(v => v.Number, SortComparerZeroLast.Default).ToList(); @@ -479,7 +478,7 @@ public class ReaderService : IReaderService var currentVolume = volumes.Single(v => v.Id == volumeId); var currentChapter = currentVolume.Chapters.Single(c => c.Id == currentChapterId); - if (currentVolume.Number == 0) + if (currentVolume.MinNumber == 0) { var chapterId = GetNextChapterId(currentVolume.Chapters.OrderByNatural(x => x.Range).Reverse(), currentChapter.Range, dto => dto.Range); @@ -489,7 +488,7 @@ public class ReaderService : IReaderService var next = false; foreach (var volume in volumes) { - if (volume.Number == currentVolume.Number) + if (volume.MinNumber == currentVolume.MinNumber) { var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => x.Number.AsDouble(), _chapterSortComparerForInChapterSorting).Reverse(), currentChapter.Range, dto => dto.Range); @@ -499,15 +498,15 @@ public class ReaderService : IReaderService } if (next) { - if (currentVolume.Number - 1 == 0) break; // If we have walked all the way to chapter volume, then we should break so logic outside can work + if (currentVolume.MinNumber - 1 == 0) break; // If we have walked all the way to chapter volume, then we should break so logic outside can work var lastChapter = volume.Chapters.MaxBy(x => x.Number.AsDouble(), _chapterSortComparerForInChapterSorting); if (lastChapter == null) return -1; return lastChapter.Id; } } - var lastVolume = volumes.MaxBy(v => v.Number); - if (currentVolume.Number == 0 && currentVolume.Number != lastVolume?.Number && lastVolume?.Chapters.Count > 1) + var lastVolume = volumes.MaxBy(v => v.MinNumber); + if (currentVolume.MinNumber == 0 && currentVolume.MinNumber != lastVolume?.MinNumber && lastVolume?.Chapters.Count > 1) { var lastChapter = lastVolume.Chapters.MaxBy(x => x.Number.AsDouble(), _chapterSortComparerForInChapterSorting); if (lastChapter == null) return -1; @@ -532,13 +531,13 @@ public class ReaderService : IReaderService if (!await _unitOfWork.AppUserProgressRepository.AnyUserProgressForSeriesAsync(seriesId, userId)) { // I think i need a way to sort volumes last - return volumes.OrderBy(v => v.Number.ToString(CultureInfo.InvariantCulture).AsDouble(), _chapterSortComparer).First().Chapters + return volumes.OrderBy(v => v.MinNumber.ToString(CultureInfo.InvariantCulture).AsDouble(), _chapterSortComparer).First().Chapters .OrderBy(c => c.Number.AsFloat()).First(); } // Loop through all chapters that are not in volume 0 var volumeChapters = volumes - .Where(v => v.Number != 0) + .Where(v => v.MinNumber != 0) .SelectMany(v => v.Chapters) .ToList(); @@ -550,7 +549,7 @@ public class ReaderService : IReaderService if (currentlyReadingChapter != null) return currentlyReadingChapter; // Order with volume 0 last so we prefer the natural order - return FindNextReadingChapter(volumes.OrderBy(v => v.Number, SortComparerZeroLast.Default) + return FindNextReadingChapter(volumes.OrderBy(v => v.MinNumber, SortComparerZeroLast.Default) .SelectMany(v => v.Chapters.OrderBy(c => c.Number.AsDouble())) .ToList()); } @@ -619,7 +618,7 @@ public class ReaderService : IReaderService public async Task MarkChaptersUntilAsRead(AppUser user, int seriesId, float chapterNumber) { var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(new List { seriesId }, true); - foreach (var volume in volumes.OrderBy(v => v.Number)) + foreach (var volume in volumes.OrderBy(v => v.MinNumber)) { var chapters = volume.Chapters .Where(c => !c.IsSpecial && Parser.MaxNumberFromRange(c.Range) <= chapterNumber) @@ -631,7 +630,7 @@ public class ReaderService : IReaderService public async Task MarkVolumesUntilAsRead(AppUser user, int seriesId, int volumeNumber) { var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(new List { seriesId }, true); - foreach (var volume in volumes.Where(v => v.Number <= volumeNumber && v.Number > 0).OrderBy(v => v.Number)) + foreach (var volume in volumes.Where(v => v.MinNumber <= volumeNumber && v.MinNumber > 0).OrderBy(v => v.MinNumber)) { await MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters); } diff --git a/API/Services/ReadingListService.cs b/API/Services/ReadingListService.cs index d5d5d3152..4f12f9df1 100644 --- a/API/Services/ReadingListService.cs +++ b/API/Services/ReadingListService.cs @@ -633,7 +633,7 @@ public class ReadingListService : IReadingListService var bookVolume = string.IsNullOrEmpty(book.Volume) ? Parser.DefaultVolume : book.Volume; - var matchingVolume = bookSeries.Volumes.Find(v => bookVolume == v.Name) ?? bookSeries.Volumes.Find(v => v.Number == 0); + var matchingVolume = bookSeries.Volumes.Find(v => bookVolume == v.Name) ?? bookSeries.Volumes.Find(v => v.MinNumber == 0); if (matchingVolume == null) { importSummary.Results.Add(new CblBookResult(book) diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index 32d4abbc8..8bd8a87d8 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -99,7 +99,7 @@ public class SeriesService : ISeriesService .FirstOrDefault(); if (minVolumeNumber != null && minChapter != null && float.TryParse(minChapter.Number, CultureInfo.InvariantCulture, out var chapNum) && - (chapNum >= minVolumeNumber.Number || chapNum == 0)) + (chapNum >= minVolumeNumber.MinNumber || chapNum == 0)) { return minVolumeNumber.Chapters.MinBy(c => c.Number.AsFloat(), ChapterSortComparer.Default); } @@ -521,7 +521,7 @@ public class SeriesService : ISeriesService } else { - processedVolumes = volumes.Where(v => v.Number > 0).ToList(); + processedVolumes = volumes.Where(v => v.MinNumber > 0).ToList(); processedVolumes.ForEach(v => { v.Name = $"Volume {v.Name}"; @@ -532,7 +532,7 @@ public class SeriesService : ISeriesService var specials = new List(); var chapters = volumes.SelectMany(v => v.Chapters.Select(c => { - if (v.Number == 0) return c; + if (v.MinNumber == 0) return c; c.VolumeTitle = v.Name; return c; }).OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default)).ToList(); @@ -558,7 +558,7 @@ public class SeriesService : ISeriesService } var storylineChapters = volumes - .Where(v => v.Number == 0) + .Where(v => v.MinNumber == 0) .SelectMany(v => v.Chapters.Where(c => !c.IsSpecial)) .OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default) .ToList(); @@ -799,7 +799,7 @@ public class SeriesService : ISeriesService float.TryParse(lastChapter.Number, NumberStyles.Number, CultureInfo.InvariantCulture, out var lastChapterNumber); - var lastVolumeNum = chapters.Select(c => c.Volume.Number).Max(); + var lastVolumeNum = chapters.Select(c => c.Volume.MinNumber).Max(); var result = new NextExpectedChapterDto { @@ -812,7 +812,7 @@ public class SeriesService : ISeriesService if (lastChapterNumber > 0) { result.ChapterNumber = (int) Math.Truncate(lastChapterNumber) + 1; - result.VolumeNumber = lastChapter.Volume.Number; + result.VolumeNumber = lastChapter.Volume.MinNumber; result.Title = series.Library.Type switch { LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", result.ChapterNumber), diff --git a/API/Services/StatisticService.cs b/API/Services/StatisticService.cs index ffc2196d0..ad465af7b 100644 --- a/API/Services/StatisticService.cs +++ b/API/Services/StatisticService.cs @@ -287,7 +287,7 @@ public class StatisticService : IStatisticService TotalPeople = distinctPeople, TotalSize = await _context.MangaFile.SumAsync(m => m.Bytes), TotalTags = await _context.Tag.CountAsync(), - VolumeCount = await _context.Volume.Where(v => v.Number != 0).CountAsync(), + VolumeCount = await _context.Volume.Where(v => v.MinNumber != 0).CountAsync(), MostActiveUsers = mostActiveUsers, MostActiveLibraries = mostActiveLibrary, MostPopularSeries = mostPopularSeries, diff --git a/API/Services/TachiyomiService.cs b/API/Services/TachiyomiService.cs index e4e9aab21..68d4bb5ac 100644 --- a/API/Services/TachiyomiService.cs +++ b/API/Services/TachiyomiService.cs @@ -9,6 +9,7 @@ using System.Linq; using API.Comparators; using API.Entities; using API.Extensions; +using API.Services.Tasks.Scanner.Parser; using AutoMapper; using Microsoft.Extensions.Logging; @@ -68,21 +69,21 @@ public class TachiyomiService : ITachiyomiService // Else return the max chapter to Tachiyomi so it can consider everything read var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(seriesId)).ToImmutableList(); - var looseLeafChapterVolume = volumes.Find(v => v.Number == 0); + var looseLeafChapterVolume = volumes.Find(v => v.MinNumber == 0); if (looseLeafChapterVolume == null) { var volumeChapter = _mapper.Map(volumes [^1].Chapters .OrderBy(c => c.Number.AsFloat(), ChapterSortComparerZeroFirst.Default) .Last()); - if (volumeChapter.Number == "0") + if (volumeChapter.Number == Parser.DefaultVolume) { var volume = volumes.First(v => v.Id == volumeChapter.VolumeId); return new ChapterDto() { // Use R to ensure that localization of underlying system doesn't affect the stringification // https://docs.microsoft.com/en-us/globalization/locale/number-formatting-in-dotnet-framework - Number = (volume.Number / 10_000f).ToString("R", EnglishCulture) + Number = (volume.MinNumber / 10_000f).ToString("R", EnglishCulture) }; } @@ -103,14 +104,14 @@ public class TachiyomiService : ITachiyomiService var volumeWithProgress = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(prevChapter.VolumeId, userId); // We only encode for single-file volumes - if (volumeWithProgress!.Number != 0 && volumeWithProgress.Chapters.Count == 1) + if (volumeWithProgress!.MinNumber != 0 && volumeWithProgress.Chapters.Count == 1) { // The progress is on a volume, encode it as a fake chapterDTO return new ChapterDto() { // Use R to ensure that localization of underlying system doesn't affect the stringification // https://docs.microsoft.com/en-us/globalization/locale/number-formatting-in-dotnet-framework - Number = (volumeWithProgress.Number / 10_000f).ToString("R", EnglishCulture) + Number = (volumeWithProgress.MinNumber / 10_000f).ToString("R", EnglishCulture) }; } diff --git a/API/Services/Tasks/StatsService.cs b/API/Services/Tasks/StatsService.cs index affe1300a..ce45cdb28 100644 --- a/API/Services/Tasks/StatsService.cs +++ b/API/Services/Tasks/StatsService.cs @@ -283,7 +283,7 @@ public class StatsService : IStatsService .AsNoTracking() .AsSplitQuery() .MaxAsync(s => s.Volumes! - .Where(v => v.Number == 0) + .Where(v => v.MinNumber == 0) .SelectMany(v => v.Chapters!) .Count()); } diff --git a/API/Startup.cs b/API/Startup.cs index f65d5a64b..76bf66209 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -244,8 +244,10 @@ public class Startup await MigrateSmartFilterEncoding.Migrate(unitOfWork, dataContext, logger); await MigrateLibrariesToHaveAllFileTypes.Migrate(unitOfWork, dataContext, logger); + // v0.7.14 await MigrateEmailTemplates.Migrate(directoryService, logger); + await MigrateVolumeNumber.Migrate(unitOfWork, dataContext, logger); // Update the version in the DB after all migrations are run var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion); diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index 00cf114cf..5bccb649a 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -10,12 +10,12 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/UI/Web/src/app/_models/chapter.ts b/UI/Web/src/app/_models/chapter.ts index f3010fb95..33a2588cf 100644 --- a/UI/Web/src/app/_models/chapter.ts +++ b/UI/Web/src/app/_models/chapter.ts @@ -7,7 +7,7 @@ import { AgeRating } from './metadata/age-rating'; export interface Chapter { id: number; range: string; - number: string; + minNumber: string; files: Array; /** * This is used in the UI, it is not updated or sent to Backend diff --git a/UI/Web/src/app/_models/volume.ts b/UI/Web/src/app/_models/volume.ts index 48da5a510..e944438a3 100644 --- a/UI/Web/src/app/_models/volume.ts +++ b/UI/Web/src/app/_models/volume.ts @@ -3,7 +3,8 @@ import { HourEstimateRange } from './series-detail/hour-estimate-range'; export interface Volume { id: number; - number: number; + minNumber: number; + maxNumber: number; name: string; createdUtc: string; lastModifiedUtc: string; diff --git a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts index 515d9dace..eed04a27d 100644 --- a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts +++ b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts @@ -282,7 +282,7 @@ export class EditSeriesModalComponent implements OnInit { }); this.seriesVolumes.forEach(vol => { vol.volumeFiles = vol.chapters?.sort(this.utilityService.sortChapters).map((c: Chapter) => c.files.map((f: any) => { - f.chapter = c.number; + f.chapter = c.minNumber; return f; })).flat(); }); diff --git a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.html b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.html index 183d56e19..2ef7b5dfc 100644 --- a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.html +++ b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.html @@ -123,7 +123,7 @@ - + {{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}} diff --git a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts index d4a4a85e6..56d3049a3 100644 --- a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts +++ b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts @@ -182,10 +182,10 @@ export class CardDetailDrawerComponent implements OnInit { } formatChapterNumber(chapter: Chapter) { - if (chapter.number === '0') { + if (chapter.minNumber === '0') { return '1'; } - return chapter.number; + return chapter.minNumber; } performAction(action: ActionItem, chapter: Chapter) { diff --git a/UI/Web/src/app/cards/entity-title/entity-title.component.html b/UI/Web/src/app/cards/entity-title/entity-title.component.html index b4700eb64..9f7fddabf 100644 --- a/UI/Web/src/app/cards/entity-title/entity-title.component.html +++ b/UI/Web/src/app/cards/entity-title/entity-title.component.html @@ -7,9 +7,9 @@ {{seriesName.length > 0 ? seriesName + ' - ' : ''}} - {{entity.number !== 0 ? (isChapter && includeVolume ? volumeTitle : '') : ''}} + {{entity.minNumber !== 0 ? (isChapter && includeVolume ? volumeTitle : '') : ''}} - {{entity.number !== 0 ? (isChapter ? t('issue-num') + entity.number : volumeTitle) : t('special')}} + {{entity.minNumber !== 0 ? (isChapter ? t('issue-num') + entity.minNumber : volumeTitle) : t('special')}} @@ -19,9 +19,9 @@ {{seriesName.length > 0 ? seriesName + ' - ' : ''}} - {{entity.number !== 0 ? (isChapter && includeVolume ? volumeTitle : '') : ''}} + {{entity.minNumber !== 0 ? (isChapter && includeVolume ? volumeTitle : '') : ''}} - {{entity.number !== 0 ? (isChapter ? (t('chapter') + ' ') + entity.number : volumeTitle) : t('special')}} + {{entity.minNumber !== 0 ? (isChapter ? (t('chapter') + ' ') + entity.minNumber : volumeTitle) : t('special')}} diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts index 1d30aa9ae..3c4054433 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts @@ -226,7 +226,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { /** * Track by function for Chapter to tell when to refresh card data */ - trackByChapterIdentity = (index: number, item: Chapter) => `${item.title}_${item.number}_${item.volumeId}_${item.pagesRead}`; + trackByChapterIdentity = (index: number, item: Chapter) => `${item.title}_${item.minNumber}_${item.volumeId}_${item.pagesRead}`; trackByRelatedSeriesIdentify = (index: number, item: RelatedSeriesPair) => `${item.series.name}_${item.series.libraryId}_${item.series.pagesRead}_${item.relation}`; trackBySeriesIdentify = (index: number, item: Series) => `${item.name}_${item.libraryId}_${item.pagesRead}`; trackByStoryLineIdentity = (index: number, item: StoryLineItem) => { @@ -341,13 +341,13 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { // This is a lone chapter if (vol.length === 0) { - return 'Ch ' + this.currentlyReadingChapter.number; + return 'Ch ' + this.currentlyReadingChapter.minNumber; } - if (this.currentlyReadingChapter.number === "0") { - return 'Vol ' + vol[0].number; + if (this.currentlyReadingChapter.minNumber === "0") { + return 'Vol ' + vol[0].minNumber; } - return 'Vol ' + vol[0].number + ' Ch ' + this.currentlyReadingChapter.number; + return 'Vol ' + vol[0].minNumber + ' Ch ' + this.currentlyReadingChapter.minNumber; } return this.currentlyReadingChapter.title; diff --git a/UI/Web/src/app/shared/_services/download.service.ts b/UI/Web/src/app/shared/_services/download.service.ts index 0ff8715da..673906a52 100644 --- a/UI/Web/src/app/shared/_services/download.service.ts +++ b/UI/Web/src/app/shared/_services/download.service.ts @@ -113,9 +113,9 @@ export class DownloadService { case 'series': return (downloadEntity as Series).name; case 'volume': - return (downloadEntity as Volume).number + ''; + return (downloadEntity as Volume).minNumber + ''; case 'chapter': - return (downloadEntity as Chapter).number; + return (downloadEntity as Chapter).minNumber; case 'bookmark': return ''; case 'logs': diff --git a/UI/Web/src/app/shared/_services/utility.service.ts b/UI/Web/src/app/shared/_services/utility.service.ts index 8c2382cb5..0c6577823 100644 --- a/UI/Web/src/app/shared/_services/utility.service.ts +++ b/UI/Web/src/app/shared/_services/utility.service.ts @@ -43,7 +43,7 @@ export class UtilityService { sortChapters = (a: Chapter, b: Chapter) => { - return parseFloat(a.number) - parseFloat(b.number); + return parseFloat(a.minNumber) - parseFloat(b.minNumber); } mangaFormatToText(format: MangaFormat): string { diff --git a/openapi.json b/openapi.json index e4d121d2c..36979837f 100644 --- a/openapi.json +++ b/openapi.json @@ -7,7 +7,7 @@ "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "0.7.13.6" + "version": "0.7.13.7" }, "servers": [ { @@ -16338,8 +16338,8 @@ "format": "float" }, "volumeNumber": { - "type": "integer", - "format": "int32" + "type": "number", + "format": "float" }, "expectedDate": { "type": "string", @@ -20245,7 +20245,18 @@ "number": { "type": "integer", "description": "The minimum number in the Name field in Int form", - "format": "int32" + "format": "int32", + "deprecated": true + }, + "minNumber": { + "type": "number", + "description": "The minimum number in the Name field", + "format": "float" + }, + "maxNumber": { + "type": "number", + "description": "The maximum number in the Name field (same as Minimum if Name isn't a range)", + "format": "float" }, "chapters": { "type": "array", @@ -20314,9 +20325,13 @@ "type": "integer", "format": "int32" }, - "number": { - "type": "integer", - "format": "int32" + "minNumber": { + "type": "number", + "format": "float" + }, + "maxNumber": { + "type": "number", + "format": "float" }, "name": { "type": "string",