Float-based Volumes (#2659)

This commit is contained in:
Joe Milazzo 2024-01-28 11:37:38 -06:00 committed by GitHub
parent 6fdc9228df
commit f6af6d66be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 3106 additions and 184 deletions

View File

@ -25,7 +25,7 @@ public class TestBenchmark
{ {
list.Add(new VolumeDto() list.Add(new VolumeDto()
{ {
Number = random.Next(10) > 5 ? 1 : 0, MinNumber = random.Next(10) > 5 ? 1 : 0,
Chapters = GenerateChapters() Chapters = GenerateChapters()
}); });
} }
@ -49,7 +49,7 @@ public class TestBenchmark
private static void SortSpecialChapters(IEnumerable<VolumeDto> volumes) private static void SortSpecialChapters(IEnumerable<VolumeDto> 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(); v.Chapters = v.Chapters.OrderByNatural(x => x.Range).ToList();
} }

View File

@ -192,7 +192,7 @@ public class SeriesExtensionsTests
.Build()) .Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("0") .WithChapter(new ChapterBuilder("0")
.WithIsSpecial(false) .WithIsSpecial(false)
.WithCoverImage("Volume 1") .WithCoverImage("Volume 1")
@ -229,7 +229,7 @@ public class SeriesExtensionsTests
.Build()) .Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("0") .WithChapter(new ChapterBuilder("0")
.WithIsSpecial(false) .WithIsSpecial(false)
.WithCoverImage("Volume 1") .WithCoverImage("Volume 1")
@ -266,14 +266,14 @@ public class SeriesExtensionsTests
.Build()) .Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("0") .WithChapter(new ChapterBuilder("0")
.WithIsSpecial(false) .WithIsSpecial(false)
.WithCoverImage("Volume 1") .WithCoverImage("Volume 1")
.Build()) .Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("137") .WithVolume(new VolumeBuilder("137")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("0") .WithChapter(new ChapterBuilder("0")
.WithIsSpecial(false) .WithIsSpecial(false)
.WithCoverImage("Volume 137") .WithCoverImage("Volume 137")
@ -306,7 +306,7 @@ public class SeriesExtensionsTests
.Build()) .Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("4") .WithVolume(new VolumeBuilder("4")
.WithNumber(4) .WithMinNumber(4)
.WithChapter(new ChapterBuilder("0") .WithChapter(new ChapterBuilder("0")
.WithIsSpecial(false) .WithIsSpecial(false)
.WithCoverImage("Volume 4") .WithCoverImage("Volume 4")

View File

@ -27,7 +27,7 @@ public class VolumeListExtensionsTests
.Build(), .Build(),
}; };
Assert.Equal(volumes[0].Number, volumes.GetCoverImage(MangaFormat.Archive).Number); Assert.Equal(volumes[0].MinNumber, volumes.GetCoverImage(MangaFormat.Archive).MinNumber);
} }
[Fact] [Fact]

View File

@ -180,7 +180,7 @@ Substitute.For<IMediaConversionService>());
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithFormat(MangaFormat.Epub) .WithFormat(MangaFormat.Epub)
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("0") .WithChapter(new ChapterBuilder("0")
.Build()) .Build())
.Build()) .Build())
@ -246,7 +246,7 @@ Substitute.For<IMediaConversionService>());
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithFormat(MangaFormat.Epub) .WithFormat(MangaFormat.Epub)
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("1") .WithChapter(new ChapterBuilder("1")
.Build()) .Build())
.Build()) .Build())
@ -322,7 +322,7 @@ Substitute.For<IMediaConversionService>());
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithFormat(MangaFormat.Epub) .WithFormat(MangaFormat.Epub)
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("1") .WithChapter(new ChapterBuilder("1")
.Build()) .Build())
.Build()) .Build())
@ -375,7 +375,7 @@ Substitute.For<IMediaConversionService>());
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithFormat(MangaFormat.Epub) .WithFormat(MangaFormat.Epub)
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("1") .WithChapter(new ChapterBuilder("1")
.Build()) .Build())
.Build()) .Build())
@ -428,7 +428,7 @@ Substitute.For<IMediaConversionService>());
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithFormat(MangaFormat.Epub) .WithFormat(MangaFormat.Epub)
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("1") .WithChapter(new ChapterBuilder("1")
.Build()) .Build())
.Build()) .Build())

View File

@ -395,7 +395,7 @@ public class CleanupServiceTests : AbstractDbTest
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithFormat(MangaFormat.Epub) .WithFormat(MangaFormat.Epub)
.WithVolume(new VolumeBuilder("0") .WithVolume(new VolumeBuilder("0")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(c) .WithChapter(c)
.Build()) .Build())
.Build(); .Build();

View File

@ -136,7 +136,7 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("0") .WithVolume(new VolumeBuilder("0")
.WithNumber(0) .WithMinNumber(0)
.WithChapter(new ChapterBuilder("0") .WithChapter(new ChapterBuilder("0")
.WithPages(1) .WithPages(1)
.Build()) .Build())
@ -166,7 +166,7 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("0") .WithVolume(new VolumeBuilder("0")
.WithNumber(0) .WithMinNumber(0)
.WithChapter(new ChapterBuilder("0") .WithChapter(new ChapterBuilder("0")
.WithPages(1) .WithPages(1)
.Build()) .Build())
@ -205,7 +205,7 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("0") .WithVolume(new VolumeBuilder("0")
.WithNumber(0) .WithMinNumber(0)
.WithChapter(new ChapterBuilder("0") .WithChapter(new ChapterBuilder("0")
.WithPages(1) .WithPages(1)
.Build()) .Build())
@ -260,7 +260,7 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("0") .WithVolume(new VolumeBuilder("0")
.WithNumber(0) .WithMinNumber(0)
.WithChapter(new ChapterBuilder("0") .WithChapter(new ChapterBuilder("0")
.WithPages(1) .WithPages(1)
.Build()) .Build())
@ -299,7 +299,7 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("0") .WithVolume(new VolumeBuilder("0")
.WithNumber(0) .WithMinNumber(0)
.WithChapter(new ChapterBuilder("0") .WithChapter(new ChapterBuilder("0")
.WithPages(1) .WithPages(1)
.Build()) .Build())
@ -347,19 +347,19 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("2") .WithVolume(new VolumeBuilder("2")
.WithNumber(2) .WithMinNumber(2)
.WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("21").Build())
.WithChapter(new ChapterBuilder("22").Build()) .WithChapter(new ChapterBuilder("22").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("3") .WithVolume(new VolumeBuilder("3")
.WithNumber(3) .WithMinNumber(3)
.WithChapter(new ChapterBuilder("31").Build()) .WithChapter(new ChapterBuilder("31").Build())
.WithChapter(new ChapterBuilder("32").Build()) .WithChapter(new ChapterBuilder("32").Build())
.Build()) .Build())
@ -382,7 +382,7 @@ public class ReaderServiceTests
Assert.Equal("2", actualChapter.Range); Assert.Equal("2", actualChapter.Range);
} }
//[Fact] [Fact]
public async Task GetNextChapterIdAsync_ShouldGetNextVolume_WhenUsingRanges() public async Task GetNextChapterIdAsync_ShouldGetNextVolume_WhenUsingRanges()
{ {
// V1 -> V2 // V1 -> V2
@ -390,15 +390,13 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1-2") .WithVolume(new VolumeBuilder("1-2")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("0").Build())
.WithChapter(new ChapterBuilder("2").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("3-4") .WithVolume(new VolumeBuilder("3-4")
.WithNumber(2) .WithMinNumber(2)
.WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("22").Build())
.Build()) .Build())
.Build(); .Build();
series.Library = new LibraryBuilder("Test Lib", LibraryType.Manga).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 nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 1, 1);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); 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] [Fact]
@ -467,19 +466,19 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("2") .WithVolume(new VolumeBuilder("2")
.WithNumber(2) .WithMinNumber(2)
.WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("21").Build())
.WithChapter(new ChapterBuilder("22").Build()) .WithChapter(new ChapterBuilder("22").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("3") .WithVolume(new VolumeBuilder("3")
.WithNumber(3) .WithMinNumber(3)
.WithChapter(new ChapterBuilder("31").Build()) .WithChapter(new ChapterBuilder("31").Build())
.WithChapter(new ChapterBuilder("32").Build()) .WithChapter(new ChapterBuilder("32").Build())
.Build()) .Build())
@ -508,19 +507,19 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1.5") .WithVolume(new VolumeBuilder("1.5")
.WithNumber(2) .WithMinNumber(2)
.WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("21").Build())
.WithChapter(new ChapterBuilder("22").Build()) .WithChapter(new ChapterBuilder("22").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("3") .WithVolume(new VolumeBuilder("3")
.WithNumber(3) .WithMinNumber(3)
.WithChapter(new ChapterBuilder("31").Build()) .WithChapter(new ChapterBuilder("31").Build())
.WithChapter(new ChapterBuilder("32").Build()) .WithChapter(new ChapterBuilder("32").Build())
.Build()) .Build())
@ -550,13 +549,13 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("0") .WithVolume(new VolumeBuilder("0")
.WithNumber(0) .WithMinNumber(0)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("21").Build())
.WithChapter(new ChapterBuilder("22").Build()) .WithChapter(new ChapterBuilder("22").Build())
.Build()) .Build())
@ -585,18 +584,18 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("0") .WithVolume(new VolumeBuilder("0")
.WithNumber(0) .WithMinNumber(0)
.WithChapter(new ChapterBuilder("66").Build()) .WithChapter(new ChapterBuilder("66").Build())
.WithChapter(new ChapterBuilder("67").Build()) .WithChapter(new ChapterBuilder("67").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("2") .WithVolume(new VolumeBuilder("2")
.WithNumber(2) .WithMinNumber(2)
.WithChapter(new ChapterBuilder("0").Build()) .WithChapter(new ChapterBuilder("0").Build())
.Build()) .Build())
.Build(); .Build();
@ -627,13 +626,13 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("0") .WithVolume(new VolumeBuilder("0")
.WithNumber(0) .WithMinNumber(0)
.WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build())
.WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build())
.Build()) .Build())
@ -659,7 +658,7 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.Build()) .Build())
@ -685,7 +684,7 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("0") .WithVolume(new VolumeBuilder("0")
.WithNumber(0) .WithMinNumber(0)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.Build()) .Build())
@ -712,14 +711,14 @@ public class ReaderServiceTests
// //
// var series = new SeriesBuilder("Test") // var series = new SeriesBuilder("Test")
// .WithVolume(new VolumeBuilder("0") // .WithVolume(new VolumeBuilder("0")
// .WithNumber(0) // .WithMinNumber(0)
// .WithChapter(new ChapterBuilder("1").Build()) // .WithChapter(new ChapterBuilder("1").Build())
// .WithChapter(new ChapterBuilder("2").Build()) // .WithChapter(new ChapterBuilder("2").Build())
// .WithChapter(new ChapterBuilder("0").WithIsSpecial(true).Build()) // .WithChapter(new ChapterBuilder("0").WithIsSpecial(true).Build())
// .Build()) // .Build())
// //
// .WithVolume(new VolumeBuilder("1") // .WithVolume(new VolumeBuilder("1")
// .WithNumber(1) // .WithMinNumber(1)
// .WithChapter(new ChapterBuilder("2").Build()) // .WithChapter(new ChapterBuilder("2").Build())
// .Build()) // .Build())
// .Build(); // .Build();
@ -746,13 +745,13 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("0") .WithVolume(new VolumeBuilder("0")
.WithNumber(0) .WithMinNumber(0)
.WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build())
.WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build())
.Build()) .Build())
@ -782,7 +781,7 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("0") .WithVolume(new VolumeBuilder("0")
.WithNumber(0) .WithMinNumber(0)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build())
@ -813,13 +812,13 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("0") .WithVolume(new VolumeBuilder("0")
.WithNumber(0) .WithMinNumber(0)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("0").Build()) .WithChapter(new ChapterBuilder("0").Build())
.Build()) .Build())
.Build(); .Build();
@ -846,12 +845,12 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("0") .WithVolume(new VolumeBuilder("0")
.WithNumber(0) .WithMinNumber(0)
.WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build())
.WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build())
.Build()) .Build())
@ -881,12 +880,12 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("12").Build()) .WithChapter(new ChapterBuilder("12").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("2") .WithVolume(new VolumeBuilder("2")
.WithNumber(2) .WithMinNumber(2)
.WithChapter(new ChapterBuilder("12").Build()) .WithChapter(new ChapterBuilder("12").Build())
.Build()) .Build())
.Build(); .Build();
@ -907,7 +906,7 @@ public class ReaderServiceTests
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 1, 1); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 1, 1);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter, ChapterIncludes.Volumes); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter, ChapterIncludes.Volumes);
Assert.Equal(2, actualChapter.Volume.Number); Assert.Equal(2, actualChapter.Volume.MinNumber);
} }
#endregion #endregion
@ -922,19 +921,19 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("2") .WithVolume(new VolumeBuilder("2")
.WithNumber(2) .WithMinNumber(2)
.WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("21").Build())
.WithChapter(new ChapterBuilder("22").Build()) .WithChapter(new ChapterBuilder("22").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("3") .WithVolume(new VolumeBuilder("3")
.WithNumber(3) .WithMinNumber(3)
.WithChapter(new ChapterBuilder("31").Build()) .WithChapter(new ChapterBuilder("31").Build())
.WithChapter(new ChapterBuilder("32").Build()) .WithChapter(new ChapterBuilder("32").Build())
.Build()) .Build())
@ -965,19 +964,19 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1.5") .WithVolume(new VolumeBuilder("1.5")
.WithNumber(2) .WithMinNumber(2)
.WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("21").Build())
.WithChapter(new ChapterBuilder("22").Build()) .WithChapter(new ChapterBuilder("22").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("3") .WithVolume(new VolumeBuilder("3")
.WithNumber(3) .WithMinNumber(3)
.WithChapter(new ChapterBuilder("31").Build()) .WithChapter(new ChapterBuilder("31").Build())
.WithChapter(new ChapterBuilder("32").Build()) .WithChapter(new ChapterBuilder("32").Build())
.Build()) .Build())
@ -1089,13 +1088,13 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("0") .WithVolume(new VolumeBuilder("0")
.WithNumber(0) .WithMinNumber(0)
.WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build())
.WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build())
.Build()) .Build())
@ -1127,7 +1126,7 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.Build()) .Build())
@ -1157,7 +1156,7 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("0").Build()) .WithChapter(new ChapterBuilder("0").Build())
.Build()) .Build())
.Build(); .Build();
@ -1186,13 +1185,13 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("0") .WithVolume(new VolumeBuilder("0")
.WithNumber(0) .WithMinNumber(0)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("0").Build()) .WithChapter(new ChapterBuilder("0").Build())
.Build()) .Build())
.Build(); .Build();
@ -1221,20 +1220,20 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("0") .WithVolume(new VolumeBuilder("0")
.WithNumber(0) .WithMinNumber(0)
.WithChapter(new ChapterBuilder("5").Build()) .WithChapter(new ChapterBuilder("5").Build())
.WithChapter(new ChapterBuilder("6").Build()) .WithChapter(new ChapterBuilder("6").Build())
.WithChapter(new ChapterBuilder("7").Build()) .WithChapter(new ChapterBuilder("7").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("1").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("1").WithIsSpecial(true).Build())
.WithChapter(new ChapterBuilder("2").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("2").WithIsSpecial(true).Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("2") .WithVolume(new VolumeBuilder("2")
.WithNumber(2) .WithMinNumber(2)
.WithChapter(new ChapterBuilder("3").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("3").WithIsSpecial(true).Build())
.WithChapter(new ChapterBuilder("4").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("4").WithIsSpecial(true).Build())
.Build()) .Build())
@ -1269,7 +1268,7 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("0") .WithVolume(new VolumeBuilder("0")
.WithNumber(0) .WithMinNumber(0)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.Build()) .Build())
@ -1299,12 +1298,12 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("0") .WithVolume(new VolumeBuilder("0")
.WithNumber(0) .WithMinNumber(0)
.WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build())
.WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build())
.Build()) .Build())
@ -1337,12 +1336,12 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("0") .WithVolume(new VolumeBuilder("0")
.WithNumber(0) .WithMinNumber(0)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("21").Build())
.WithChapter(new ChapterBuilder("22").Build()) .WithChapter(new ChapterBuilder("22").Build())
.Build()) .Build())
@ -1375,12 +1374,12 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("12").Build()) .WithChapter(new ChapterBuilder("12").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("2") .WithVolume(new VolumeBuilder("2")
.WithNumber(2) .WithMinNumber(2)
.WithChapter(new ChapterBuilder("12").Build()) .WithChapter(new ChapterBuilder("12").Build())
.Build()) .Build())
.Build(); .Build();
@ -1396,7 +1395,7 @@ public class ReaderServiceTests
var nextChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 2, 1); var nextChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 2, 1);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter, ChapterIncludes.Volumes); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter, ChapterIncludes.Volumes);
Assert.Equal(1, actualChapter.Volume.Number); Assert.Equal(1, actualChapter.Volume.MinNumber);
} }
#endregion #endregion

View File

@ -759,7 +759,7 @@ public class ReadingListServiceTests
var fablesSeries = new SeriesBuilder("Fables").Build(); var fablesSeries = new SeriesBuilder("Fables").Build();
fablesSeries.Volumes.Add( fablesSeries.Volumes.Add(
new VolumeBuilder("1") new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithName("2002") .WithName("2002")
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.Build() .Build()
@ -937,7 +937,7 @@ public class ReadingListServiceTests
var fables2Series = new SeriesBuilder("Fables: The Last Castle").Build(); var fables2Series = new SeriesBuilder("Fables: The Last Castle").Build();
fablesSeries.Volumes.Add(new VolumeBuilder("1") fablesSeries.Volumes.Add(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithName("2002") .WithName("2002")
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
@ -945,7 +945,7 @@ public class ReadingListServiceTests
.Build() .Build()
); );
fables2Series.Volumes.Add(new VolumeBuilder("1") fables2Series.Volumes.Add(new VolumeBuilder("1")
.WithNumber(1) .WithMinNumber(1)
.WithName("2003") .WithName("2003")
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
@ -980,13 +980,13 @@ public class ReadingListServiceTests
var fables2Series = new SeriesBuilder("Fablesa: The Last Castle").Build(); var fables2Series = new SeriesBuilder("Fablesa: The Last Castle").Build();
fablesSeries.Volumes.Add(new VolumeBuilder("2002") fablesSeries.Volumes.Add(new VolumeBuilder("2002")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.WithChapter(new ChapterBuilder("3").Build()) .WithChapter(new ChapterBuilder("3").Build())
.Build()); .Build());
fables2Series.Volumes.Add(new VolumeBuilder("2003") fables2Series.Volumes.Add(new VolumeBuilder("2003")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.WithChapter(new ChapterBuilder("3").Build()) .WithChapter(new ChapterBuilder("3").Build())
@ -1036,7 +1036,7 @@ public class ReadingListServiceTests
// Mock up our series // Mock up our series
var fablesSeries = new SeriesBuilder("Fables") var fablesSeries = new SeriesBuilder("Fables")
.WithVolume(new VolumeBuilder("2002") .WithVolume(new VolumeBuilder("2002")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.WithChapter(new ChapterBuilder("3").Build()) .WithChapter(new ChapterBuilder("3").Build())
@ -1045,7 +1045,7 @@ public class ReadingListServiceTests
var fables2Series = new SeriesBuilder("Fables: The Last Castle") var fables2Series = new SeriesBuilder("Fables: The Last Castle")
.WithVolume(new VolumeBuilder("2003") .WithVolume(new VolumeBuilder("2003")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.WithChapter(new ChapterBuilder("3").Build()) .WithChapter(new ChapterBuilder("3").Build())
@ -1094,13 +1094,13 @@ public class ReadingListServiceTests
var fables2Series = new SeriesBuilder("Fables: The Last Castle").Build(); var fables2Series = new SeriesBuilder("Fables: The Last Castle").Build();
fablesSeries.Volumes.Add(new VolumeBuilder("2002") fablesSeries.Volumes.Add(new VolumeBuilder("2002")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.WithChapter(new ChapterBuilder("3").Build()) .WithChapter(new ChapterBuilder("3").Build())
.Build()); .Build());
fables2Series.Volumes.Add(new VolumeBuilder("2003") fables2Series.Volumes.Add(new VolumeBuilder("2003")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.WithChapter(new ChapterBuilder("3").Build()) .WithChapter(new ChapterBuilder("3").Build())
@ -1153,13 +1153,13 @@ public class ReadingListServiceTests
var fables2Series = new SeriesBuilder("Fables: The Last Castle").Build(); var fables2Series = new SeriesBuilder("Fables: The Last Castle").Build();
fablesSeries.Volumes.Add(new VolumeBuilder("2002") fablesSeries.Volumes.Add(new VolumeBuilder("2002")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.WithChapter(new ChapterBuilder("3").Build()) .WithChapter(new ChapterBuilder("3").Build())
.Build()); .Build());
fables2Series.Volumes.Add(new VolumeBuilder("2003") fables2Series.Volumes.Add(new VolumeBuilder("2003")
.WithNumber(1) .WithMinNumber(1)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.WithChapter(new ChapterBuilder("3").Build()) .WithChapter(new ChapterBuilder("3").Build())

View File

@ -95,7 +95,7 @@
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" /> <PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
<PackageReference Include="SharpCompress" Version="0.36.0" /> <PackageReference Include="SharpCompress" Version="0.36.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.2" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.2" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.17.0.82934"> <PackageReference Include="SonarAnalyzer.CSharp" Version="9.18.0.83559">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View File

@ -103,7 +103,7 @@ public class DownloadController : BaseApiController
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
try 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) catch (KavitaException ex)
{ {

View File

@ -1109,7 +1109,7 @@ public class OpdsController : BaseApiController
title += $" - {volume.Name}"; 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)}"; title = $"{series.Name} - Volume {volume.Name} - {await _seriesService.FormatChapterTitle(userId, chapter, libraryType)}";
} }

View File

@ -5,7 +5,7 @@ namespace API.DTOs.SeriesDetail;
public class NextExpectedChapterDto public class NextExpectedChapterDto
{ {
public float ChapterNumber { get; set; } public float ChapterNumber { get; set; }
public int VolumeNumber { get; set; } public float VolumeNumber { get; set; }
/// <summary> /// <summary>
/// Null if not applicable /// Null if not applicable
/// </summary> /// </summary>

View File

@ -9,9 +9,10 @@ namespace API.DTOs;
public class VolumeDto : IHasReadTimeEstimate public class VolumeDto : IHasReadTimeEstimate
{ {
public int Id { get; set; } public int Id { get; set; }
/// <inheritdoc cref="Volume.Number"/> /// <inheritdoc cref="Volume.MinNumber"/>
public int Number { get; set; } public float MinNumber { get; set; }
/// <inheritdoc cref="Volume.MaxNumber"/>
public float MaxNumber { get; set; }
/// <inheritdoc cref="Volume.Name"/> /// <inheritdoc cref="Volume.Name"/>
public string Name { get; set; } = default!; public string Name { get; set; } = default!;
public int Pages { get; set; } public int Pages { get; set; }

View File

@ -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;
/// <summary>
/// Introduced in v0.7.14, this migrates the existing Volume Name -> Volume Min/Max Number
/// </summary>
public static class MigrateVolumeNumber
{
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> 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");
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class VolumeMinMaxNumbers : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<float>(
name: "MaxNumber",
table: "Volume",
type: "REAL",
nullable: false,
defaultValue: 0f);
migrationBuilder.AddColumn<float>(
name: "MinNumber",
table: "Volume",
type: "REAL",
nullable: false,
defaultValue: 0f);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "MaxNumber",
table: "Volume");
migrationBuilder.DropColumn(
name: "MinNumber",
table: "Volume");
}
}
}

View File

@ -1781,9 +1781,15 @@ namespace API.Data.Migrations
b.Property<int>("MaxHoursToRead") b.Property<int>("MaxHoursToRead")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<float>("MaxNumber")
.HasColumnType("REAL");
b.Property<int>("MinHoursToRead") b.Property<int>("MinHoursToRead")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<float>("MinNumber")
.HasColumnType("REAL");
b.Property<string>("Name") b.Property<string>("Name")
.HasColumnType("TEXT"); .HasColumnType("TEXT");

View File

@ -18,6 +18,6 @@ public class RecentlyAddedSeries
public string? ChapterRange { get; init; } public string? ChapterRange { get; init; }
public string? ChapterTitle { get; init; } public string? ChapterTitle { get; init; }
public bool IsSpecial { get; init; } public bool IsSpecial { get; init; }
public int VolumeNumber { get; init; } public float VolumeNumber { get; init; }
public AgeRating AgeRating { get; init; } public AgeRating AgeRating { get; init; }
} }

View File

@ -32,7 +32,7 @@ public interface IAppUserProgressRepository
Task<ProgressDto?> GetUserProgressDtoAsync(int chapterId, int userId); Task<ProgressDto?> GetUserProgressDtoAsync(int chapterId, int userId);
Task<bool> AnyUserProgressForSeriesAsync(int seriesId, int userId); Task<bool> AnyUserProgressForSeriesAsync(int seriesId, int userId);
Task<int> GetHighestFullyReadChapterForSeries(int seriesId, int userId); Task<int> GetHighestFullyReadChapterForSeries(int seriesId, int userId);
Task<int> GetHighestFullyReadVolumeForSeries(int seriesId, int userId); Task<float> GetHighestFullyReadVolumeForSeries(int seriesId, int userId);
Task<DateTime?> GetLatestProgressForSeries(int seriesId, int userId); Task<DateTime?> GetLatestProgressForSeries(int seriesId, int userId);
Task<DateTime?> GetFirstProgressForSeries(int seriesId, int userId); Task<DateTime?> GetFirstProgressForSeries(int seriesId, int userId);
Task UpdateAllProgressThatAreMoreThanChapterPages(); 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))); return list.Count == 0 ? 0 : list.DefaultIfEmpty().Where(d => d != null).Max(d => (int) Math.Floor(Parser.MaxNumberFromRange(d)));
} }
public async Task<int> GetHighestFullyReadVolumeForSeries(int seriesId, int userId) public async Task<float> GetHighestFullyReadVolumeForSeries(int seriesId, int userId)
{ {
var list = await _context.AppUserProgresses var list = await _context.AppUserProgresses
.Join(_context.Chapter, appUserProgresses => appUserProgresses.ChapterId, chapter => chapter.Id, .Join(_context.Chapter, appUserProgresses => appUserProgresses.ChapterId, chapter => chapter.Id,
(appUserProgresses, chapter) => new {appUserProgresses, chapter}) (appUserProgresses, chapter) => new {appUserProgresses, chapter})
.Where(p => p.appUserProgresses.SeriesId == seriesId && p.appUserProgresses.AppUserId == userId && .Where(p => p.appUserProgresses.SeriesId == seriesId && p.appUserProgresses.AppUserId == userId &&
p.appUserProgresses.PagesRead >= p.chapter.Pages) p.appUserProgresses.PagesRead >= p.chapter.Pages)
.Select(p => p.chapter.Volume.Number) .Select(p => p.chapter.Volume.MaxNumber)
.ToListAsync(); .ToListAsync();
return list.Count == 0 ? 0 : list.DefaultIfEmpty().Max(); return list.Count == 0 ? 0 : list.DefaultIfEmpty().Max();
} }

View File

@ -1836,7 +1836,7 @@ public class SeriesRepository : ISeriesRepository
ChapterNumber = c.Number, ChapterNumber = c.Number,
ChapterRange = c.Range, ChapterRange = c.Range,
IsSpecial = c.IsSpecial, IsSpecial = c.IsSpecial,
VolumeNumber = c.Volume.Number, VolumeNumber = c.Volume.MinNumber,
ChapterTitle = c.Title, ChapterTitle = c.Title,
AgeRating = c.Volume.Series.Metadata.AgeRating AgeRating = c.Volume.Series.Metadata.AgeRating
}) })

View File

@ -152,7 +152,7 @@ public class VolumeRepository : IVolumeRepository
.Include(vol => vol.Chapters) .Include(vol => vol.Chapters)
.ThenInclude(c => c.Files) .ThenInclude(c => c.Files)
.AsSplitQuery() .AsSplitQuery()
.OrderBy(vol => vol.Number) .OrderBy(vol => vol.MinNumber)
.ToListAsync(); .ToListAsync();
} }
@ -185,7 +185,7 @@ public class VolumeRepository : IVolumeRepository
.ThenInclude(c => c.People) .ThenInclude(c => c.People)
.Include(vol => vol.Chapters) .Include(vol => vol.Chapters)
.ThenInclude(c => c.Tags) .ThenInclude(c => c.Tags)
.OrderBy(volume => volume.Number) .OrderBy(volume => volume.MinNumber)
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider) .ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
.AsNoTracking() .AsNoTracking()
.AsSplitQuery() .AsSplitQuery()
@ -215,7 +215,7 @@ public class VolumeRepository : IVolumeRepository
private static void SortSpecialChapters(IEnumerable<VolumeDto> volumes) private static void SortSpecialChapters(IEnumerable<VolumeDto> 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(); v.Chapters = v.Chapters.OrderByNatural(x => x.Range).ToList();
} }

View File

@ -36,7 +36,7 @@ public class ScrobbleEvent : IEntityDate
/// <summary> /// <summary>
/// Depends on the ScrobbleEvent if filled in /// Depends on the ScrobbleEvent if filled in
/// </summary> /// </summary>
public int? VolumeNumber { get; set; } public int? VolumeNumber { get; set; } // TODO: Migrate this to float
/// <summary> /// <summary>
/// Has this event been processed and pushed to Provider /// Has this event been processed and pushed to Provider
/// </summary> /// </summary>

View File

@ -15,7 +15,17 @@ public class Volume : IEntityDate, IHasReadTimeEstimate
/// <summary> /// <summary>
/// The minimum number in the Name field in Int form /// The minimum number in the Name field in Int form
/// </summary> /// </summary>
public required int Number { get; set; } /// <remarks>Removed in v0.7.13.8, this was an int and we need the ability to have 0.5 volumes render on the UI</remarks>
[Obsolete("Use MinNumber and MaxNumber instead")]
public int Number { get; set; }
/// <summary>
/// The minimum number in the Name field
/// </summary>
public required float MinNumber { get; set; }
/// <summary>
/// The maximum number in the Name field (same as Minimum if Name isn't a range)
/// </summary>
public required float MaxNumber { get; set; }
public IList<Chapter> Chapters { get; set; } = null!; public IList<Chapter> Chapters { get; set; } = null!;
public DateTime Created { get; set; } public DateTime Created { get; set; }
public DateTime LastModified { get; set; } public DateTime LastModified { get; set; }

View File

@ -18,8 +18,8 @@ public static class SeriesExtensions
/// <remarks>This is under the assumption that the Volume already has a Cover Image calculated and set</remarks> /// <remarks>This is under the assumption that the Volume already has a Cover Image calculated and set</remarks>
public static string? GetCoverImage(this Series series) public static string? GetCoverImage(this Series series)
{ {
var volumes = (series.Volumes ?? new List<Volume>()) var volumes = (series.Volumes ?? [])
.OrderBy(v => v.Number, ChapterSortComparer.Default) .OrderBy(v => v.MinNumber, ChapterSortComparer.Default)
.ToList(); .ToList();
var firstVolume = volumes.GetCoverImage(series.Format); var firstVolume = volumes.GetCoverImage(series.Format);
if (firstVolume == null) return null; if (firstVolume == null) return null;
@ -34,20 +34,20 @@ public static class SeriesExtensions
} }
// just volumes // just volumes
if (volumes.TrueForAll(v => $"{v.Number}" != Parser.DefaultVolume)) if (volumes.TrueForAll(v => $"{v.MinNumber}" != Parser.DefaultVolume))
{ {
return firstVolume.CoverImage; return firstVolume.CoverImage;
} }
// If we have loose leaf chapters // If we have loose leaf chapters
// if loose leaf chapters AND volumes, just return first volume // 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)) .SelectMany(c => c.Chapters.Where(c => !c.IsSpecial))
.OrderBy(c => c.Number.AsDouble(), ChapterSortComparerZeroFirst.Default) .OrderBy(c => c.Number.AsDouble(), ChapterSortComparerZeroFirst.Default)
.ToList(); .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; return looseLeafChapters[0].CoverImage;
} }
@ -55,7 +55,7 @@ public static class SeriesExtensions
} }
var firstLooseLeafChapter = volumes var firstLooseLeafChapter = volumes
.Where(v => $"{v.Number}" == Parser.DefaultVolume) .Where(v => $"{v.MinNumber}" == Parser.DefaultVolume)
.SelectMany(v => v.Chapters) .SelectMany(v => v.Chapters)
.OrderBy(c => c.Number.AsDouble(), ChapterSortComparerZeroFirst.Default) .OrderBy(c => c.Number.AsDouble(), ChapterSortComparerZeroFirst.Default)
.FirstOrDefault(c => !c.IsSpecial); .FirstOrDefault(c => !c.IsSpecial);

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using API.Entities; using API.Entities;
using API.Entities.Enums; using API.Entities.Enums;
using API.Services.Tasks.Scanner.Parser;
namespace API.Extensions; namespace API.Extensions;
#nullable enable #nullable enable
@ -22,16 +23,16 @@ public static class VolumeListExtensions
if (seriesFormat == MangaFormat.Epub || seriesFormat == MangaFormat.Pdf) 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 // 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);
} }
} }

View File

@ -15,8 +15,8 @@ public class VolumeBuilder : IEntityBuilder<Volume>
_volume = new Volume() _volume = new Volume()
{ {
Name = volumeNumber, Name = volumeNumber,
// TODO / BUG: Try to use float based Number which will allow Epub's with < 1 volumes to show in series detail MinNumber = Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(volumeNumber),
Number = (int) Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(volumeNumber), MaxNumber = Services.Tasks.Scanner.Parser.Parser.MaxNumberFromRange(volumeNumber),
Chapters = new List<Chapter>() Chapters = new List<Chapter>()
}; };
} }
@ -27,9 +27,25 @@ public class VolumeBuilder : IEntityBuilder<Volume>
return this; 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; return this;
} }

View File

@ -298,7 +298,7 @@ public class ScrobblingService : IScrobblingService
var prevVol = $"{existingEvt.VolumeNumber}"; var prevVol = $"{existingEvt.VolumeNumber}";
existingEvt.VolumeNumber = existingEvt.VolumeNumber =
await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId); (int) await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId);
existingEvt.ChapterNumber = existingEvt.ChapterNumber =
await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(seriesId, userId); await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(seriesId, userId);
_unitOfWork.ScrobbleRepository.Update(existingEvt); _unitOfWork.ScrobbleRepository.Update(existingEvt);
@ -319,7 +319,7 @@ public class ScrobblingService : IScrobblingService
MalId = ExtractId<long?>(series.Metadata.WebLinks, MalWeblinkWebsite), MalId = ExtractId<long?>(series.Metadata.WebLinks, MalWeblinkWebsite),
AppUserId = userId, AppUserId = userId,
VolumeNumber = VolumeNumber =
await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId), (int) await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId),
ChapterNumber = ChapterNumber =
await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(seriesId, userId), await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(seriesId, userId),
Format = LibraryTypeHelper.GetFormat(series.Library.Type), Format = LibraryTypeHelper.GetFormat(series.Library.Type),
@ -660,7 +660,7 @@ public class ScrobblingService : IScrobblingService
foreach (var readEvt in readEvents) foreach (var readEvt in readEvents)
{ {
readEvt.VolumeNumber = readEvt.VolumeNumber =
await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(readEvt.SeriesId, (int) await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(readEvt.SeriesId,
readEvt.AppUser.Id); readEvt.AppUser.Id);
readEvt.ChapterNumber = readEvt.ChapterNumber =
await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(readEvt.SeriesId, await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(readEvt.SeriesId,

View File

@ -365,18 +365,17 @@ public class ReaderService : IReaderService
var currentVolume = volumes.Single(v => v.Id == volumeId); var currentVolume = volumes.Single(v => v.Id == volumeId);
var currentChapter = currentVolume.Chapters.Single(c => c.Id == currentChapterId); 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 // Handle specials by sorting on their Filename aka Range
var chapterId = GetNextChapterId(currentVolume.Chapters.OrderByNatural(x => x.Range), currentChapter.Range, dto => dto.Range); var chapterId = GetNextChapterId(currentVolume.Chapters.OrderByNatural(x => x.Range), currentChapter.Range, dto => dto.Range);
if (chapterId > 0) return chapterId; if (chapterId > 0) return chapterId;
} }
var currentVolumeNumber = currentVolume.Name.AsFloat();
var next = false; var next = false;
foreach (var volume in volumes) 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) if (volumeNumbersMatch && volume.Chapters.Count > 1)
{ {
// Handle Chapters within current Volume // Handle Chapters within current Volume
@ -420,9 +419,9 @@ public class ReaderService : IReaderService
else if (firstChapter.Number.AsDouble() == 0) return firstChapter.Id; else if (firstChapter.Number.AsDouble() == 0) return firstChapter.Id;
// If on last volume AND there are no specials left, then let's return -1 // 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(); .SelectMany(v => v.Chapters.Where(c => c.IsSpecial)).Any();
if (currentVolume.Number != 0 && !anySpecials) if (currentVolume.MinNumber != 0 && !anySpecials)
{ {
return -1; return -1;
} }
@ -434,10 +433,10 @@ public class ReaderService : IReaderService
// This has an added problem that it will loop up to the beginning always // 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) // 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(); 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 // 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(); // 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 currentVolume = volumes.Single(v => v.Id == volumeId);
var currentChapter = currentVolume.Chapters.Single(c => c.Id == currentChapterId); 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, var chapterId = GetNextChapterId(currentVolume.Chapters.OrderByNatural(x => x.Range).Reverse(), currentChapter.Range,
dto => dto.Range); dto => dto.Range);
@ -489,7 +488,7 @@ public class ReaderService : IReaderService
var next = false; var next = false;
foreach (var volume in volumes) 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(), var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => x.Number.AsDouble(), _chapterSortComparerForInChapterSorting).Reverse(),
currentChapter.Range, dto => dto.Range); currentChapter.Range, dto => dto.Range);
@ -499,15 +498,15 @@ public class ReaderService : IReaderService
} }
if (next) 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); var lastChapter = volume.Chapters.MaxBy(x => x.Number.AsDouble(), _chapterSortComparerForInChapterSorting);
if (lastChapter == null) return -1; if (lastChapter == null) return -1;
return lastChapter.Id; return lastChapter.Id;
} }
} }
var lastVolume = volumes.MaxBy(v => v.Number); var lastVolume = volumes.MaxBy(v => v.MinNumber);
if (currentVolume.Number == 0 && currentVolume.Number != lastVolume?.Number && lastVolume?.Chapters.Count > 1) if (currentVolume.MinNumber == 0 && currentVolume.MinNumber != lastVolume?.MinNumber && lastVolume?.Chapters.Count > 1)
{ {
var lastChapter = lastVolume.Chapters.MaxBy(x => x.Number.AsDouble(), _chapterSortComparerForInChapterSorting); var lastChapter = lastVolume.Chapters.MaxBy(x => x.Number.AsDouble(), _chapterSortComparerForInChapterSorting);
if (lastChapter == null) return -1; if (lastChapter == null) return -1;
@ -532,13 +531,13 @@ public class ReaderService : IReaderService
if (!await _unitOfWork.AppUserProgressRepository.AnyUserProgressForSeriesAsync(seriesId, userId)) if (!await _unitOfWork.AppUserProgressRepository.AnyUserProgressForSeriesAsync(seriesId, userId))
{ {
// I think i need a way to sort volumes last // 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(); .OrderBy(c => c.Number.AsFloat()).First();
} }
// Loop through all chapters that are not in volume 0 // Loop through all chapters that are not in volume 0
var volumeChapters = volumes var volumeChapters = volumes
.Where(v => v.Number != 0) .Where(v => v.MinNumber != 0)
.SelectMany(v => v.Chapters) .SelectMany(v => v.Chapters)
.ToList(); .ToList();
@ -550,7 +549,7 @@ public class ReaderService : IReaderService
if (currentlyReadingChapter != null) return currentlyReadingChapter; if (currentlyReadingChapter != null) return currentlyReadingChapter;
// Order with volume 0 last so we prefer the natural order // 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())) .SelectMany(v => v.Chapters.OrderBy(c => c.Number.AsDouble()))
.ToList()); .ToList());
} }
@ -619,7 +618,7 @@ public class ReaderService : IReaderService
public async Task MarkChaptersUntilAsRead(AppUser user, int seriesId, float chapterNumber) public async Task MarkChaptersUntilAsRead(AppUser user, int seriesId, float chapterNumber)
{ {
var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(new List<int> { seriesId }, true); var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(new List<int> { seriesId }, true);
foreach (var volume in volumes.OrderBy(v => v.Number)) foreach (var volume in volumes.OrderBy(v => v.MinNumber))
{ {
var chapters = volume.Chapters var chapters = volume.Chapters
.Where(c => !c.IsSpecial && Parser.MaxNumberFromRange(c.Range) <= chapterNumber) .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) public async Task MarkVolumesUntilAsRead(AppUser user, int seriesId, int volumeNumber)
{ {
var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(new List<int> { seriesId }, true); var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(new List<int> { 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); await MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters);
} }

View File

@ -633,7 +633,7 @@ public class ReadingListService : IReadingListService
var bookVolume = string.IsNullOrEmpty(book.Volume) var bookVolume = string.IsNullOrEmpty(book.Volume)
? Parser.DefaultVolume ? Parser.DefaultVolume
: book.Volume; : 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) if (matchingVolume == null)
{ {
importSummary.Results.Add(new CblBookResult(book) importSummary.Results.Add(new CblBookResult(book)

View File

@ -99,7 +99,7 @@ public class SeriesService : ISeriesService
.FirstOrDefault(); .FirstOrDefault();
if (minVolumeNumber != null && minChapter != null && float.TryParse(minChapter.Number, CultureInfo.InvariantCulture, out var chapNum) && 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); return minVolumeNumber.Chapters.MinBy(c => c.Number.AsFloat(), ChapterSortComparer.Default);
} }
@ -521,7 +521,7 @@ public class SeriesService : ISeriesService
} }
else else
{ {
processedVolumes = volumes.Where(v => v.Number > 0).ToList(); processedVolumes = volumes.Where(v => v.MinNumber > 0).ToList();
processedVolumes.ForEach(v => processedVolumes.ForEach(v =>
{ {
v.Name = $"Volume {v.Name}"; v.Name = $"Volume {v.Name}";
@ -532,7 +532,7 @@ public class SeriesService : ISeriesService
var specials = new List<ChapterDto>(); var specials = new List<ChapterDto>();
var chapters = volumes.SelectMany(v => v.Chapters.Select(c => 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; c.VolumeTitle = v.Name;
return c; return c;
}).OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default)).ToList(); }).OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default)).ToList();
@ -558,7 +558,7 @@ public class SeriesService : ISeriesService
} }
var storylineChapters = volumes var storylineChapters = volumes
.Where(v => v.Number == 0) .Where(v => v.MinNumber == 0)
.SelectMany(v => v.Chapters.Where(c => !c.IsSpecial)) .SelectMany(v => v.Chapters.Where(c => !c.IsSpecial))
.OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default) .OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default)
.ToList(); .ToList();
@ -799,7 +799,7 @@ public class SeriesService : ISeriesService
float.TryParse(lastChapter.Number, NumberStyles.Number, CultureInfo.InvariantCulture, float.TryParse(lastChapter.Number, NumberStyles.Number, CultureInfo.InvariantCulture,
out var lastChapterNumber); 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 var result = new NextExpectedChapterDto
{ {
@ -812,7 +812,7 @@ public class SeriesService : ISeriesService
if (lastChapterNumber > 0) if (lastChapterNumber > 0)
{ {
result.ChapterNumber = (int) Math.Truncate(lastChapterNumber) + 1; result.ChapterNumber = (int) Math.Truncate(lastChapterNumber) + 1;
result.VolumeNumber = lastChapter.Volume.Number; result.VolumeNumber = lastChapter.Volume.MinNumber;
result.Title = series.Library.Type switch result.Title = series.Library.Type switch
{ {
LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", result.ChapterNumber), LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", result.ChapterNumber),

View File

@ -287,7 +287,7 @@ public class StatisticService : IStatisticService
TotalPeople = distinctPeople, TotalPeople = distinctPeople,
TotalSize = await _context.MangaFile.SumAsync(m => m.Bytes), TotalSize = await _context.MangaFile.SumAsync(m => m.Bytes),
TotalTags = await _context.Tag.CountAsync(), 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, MostActiveUsers = mostActiveUsers,
MostActiveLibraries = mostActiveLibrary, MostActiveLibraries = mostActiveLibrary,
MostPopularSeries = mostPopularSeries, MostPopularSeries = mostPopularSeries,

View File

@ -9,6 +9,7 @@ using System.Linq;
using API.Comparators; using API.Comparators;
using API.Entities; using API.Entities;
using API.Extensions; using API.Extensions;
using API.Services.Tasks.Scanner.Parser;
using AutoMapper; using AutoMapper;
using Microsoft.Extensions.Logging; 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 // Else return the max chapter to Tachiyomi so it can consider everything read
var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(seriesId)).ToImmutableList(); var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(seriesId)).ToImmutableList();
var looseLeafChapterVolume = volumes.Find(v => v.Number == 0); var looseLeafChapterVolume = volumes.Find(v => v.MinNumber == 0);
if (looseLeafChapterVolume == null) if (looseLeafChapterVolume == null)
{ {
var volumeChapter = _mapper.Map<ChapterDto>(volumes var volumeChapter = _mapper.Map<ChapterDto>(volumes
[^1].Chapters [^1].Chapters
.OrderBy(c => c.Number.AsFloat(), ChapterSortComparerZeroFirst.Default) .OrderBy(c => c.Number.AsFloat(), ChapterSortComparerZeroFirst.Default)
.Last()); .Last());
if (volumeChapter.Number == "0") if (volumeChapter.Number == Parser.DefaultVolume)
{ {
var volume = volumes.First(v => v.Id == volumeChapter.VolumeId); var volume = volumes.First(v => v.Id == volumeChapter.VolumeId);
return new ChapterDto() return new ChapterDto()
{ {
// Use R to ensure that localization of underlying system doesn't affect the stringification // 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 // 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); var volumeWithProgress = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(prevChapter.VolumeId, userId);
// We only encode for single-file volumes // 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 // The progress is on a volume, encode it as a fake chapterDTO
return new ChapterDto() return new ChapterDto()
{ {
// Use R to ensure that localization of underlying system doesn't affect the stringification // 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 // 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)
}; };
} }

View File

@ -283,7 +283,7 @@ public class StatsService : IStatsService
.AsNoTracking() .AsNoTracking()
.AsSplitQuery() .AsSplitQuery()
.MaxAsync(s => s.Volumes! .MaxAsync(s => s.Volumes!
.Where(v => v.Number == 0) .Where(v => v.MinNumber == 0)
.SelectMany(v => v.Chapters!) .SelectMany(v => v.Chapters!)
.Count()); .Count());
} }

View File

@ -244,8 +244,10 @@ public class Startup
await MigrateSmartFilterEncoding.Migrate(unitOfWork, dataContext, logger); await MigrateSmartFilterEncoding.Migrate(unitOfWork, dataContext, logger);
await MigrateLibrariesToHaveAllFileTypes.Migrate(unitOfWork, dataContext, logger); await MigrateLibrariesToHaveAllFileTypes.Migrate(unitOfWork, dataContext, logger);
// v0.7.14 // v0.7.14
await MigrateEmailTemplates.Migrate(directoryService, logger); await MigrateEmailTemplates.Migrate(directoryService, logger);
await MigrateVolumeNumber.Migrate(unitOfWork, dataContext, logger);
// Update the version in the DB after all migrations are run // Update the version in the DB after all migrations are run
var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion); var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);

View File

@ -10,12 +10,12 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Cronos" Version="0.8.1" /> <PackageReference Include="Cronos" Version="0.8.2" />
<PackageReference Include="DotNet.Glob" Version="3.1.3" /> <PackageReference Include="DotNet.Glob" Version="3.1.3" />
<PackageReference Include="Flurl.Http" Version="3.2.4" /> <PackageReference Include="Flurl.Http" Version="3.2.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.17.0.82934"> <PackageReference Include="SonarAnalyzer.CSharp" Version="9.18.0.83559">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View File

@ -7,7 +7,7 @@ import { AgeRating } from './metadata/age-rating';
export interface Chapter { export interface Chapter {
id: number; id: number;
range: string; range: string;
number: string; minNumber: string;
files: Array<MangaFile>; files: Array<MangaFile>;
/** /**
* This is used in the UI, it is not updated or sent to Backend * This is used in the UI, it is not updated or sent to Backend

View File

@ -3,7 +3,8 @@ import { HourEstimateRange } from './series-detail/hour-estimate-range';
export interface Volume { export interface Volume {
id: number; id: number;
number: number; minNumber: number;
maxNumber: number;
name: string; name: string;
createdUtc: string; createdUtc: string;
lastModifiedUtc: string; lastModifiedUtc: string;

View File

@ -282,7 +282,7 @@ export class EditSeriesModalComponent implements OnInit {
}); });
this.seriesVolumes.forEach(vol => { this.seriesVolumes.forEach(vol => {
vol.volumeFiles = vol.chapters?.sort(this.utilityService.sortChapters).map((c: Chapter) => c.files.map((f: any) => { 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; return f;
})).flat(); })).flat();
}); });

View File

@ -123,7 +123,7 @@
<span> <span>
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions" <app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions"
[labelBy]="utilityService.formatChapterName(libraryType, true, true) + formatChapterNumber(chapter)"></app-card-actionables> [labelBy]="utilityService.formatChapterName(libraryType, true, true) + formatChapterNumber(chapter)"></app-card-actionables>
<ng-container *ngIf="chapter.number !== '0'; else specialHeader"> <ng-container *ngIf="chapter.minNumber !== '0'; else specialHeader">
{{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}} {{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
</ng-container> </ng-container>
</span> </span>

View File

@ -182,10 +182,10 @@ export class CardDetailDrawerComponent implements OnInit {
} }
formatChapterNumber(chapter: Chapter) { formatChapterNumber(chapter: Chapter) {
if (chapter.number === '0') { if (chapter.minNumber === '0') {
return '1'; return '1';
} }
return chapter.number; return chapter.minNumber;
} }
performAction(action: ActionItem<any>, chapter: Chapter) { performAction(action: ActionItem<any>, chapter: Chapter) {

View File

@ -7,9 +7,9 @@
<ng-template #fullComicTitle> <ng-template #fullComicTitle>
{{seriesName.length > 0 ? seriesName + ' - ' : ''}} {{seriesName.length > 0 ? seriesName + ' - ' : ''}}
<ng-container *ngIf="includeVolume && volumeTitle !== ''"> <ng-container *ngIf="includeVolume && volumeTitle !== ''">
{{entity.number !== 0 ? (isChapter && includeVolume ? volumeTitle : '') : ''}} {{entity.minNumber !== 0 ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
</ng-container> </ng-container>
{{entity.number !== 0 ? (isChapter ? t('issue-num') + entity.number : volumeTitle) : t('special')}} {{entity.minNumber !== 0 ? (isChapter ? t('issue-num') + entity.minNumber : volumeTitle) : t('special')}}
</ng-template> </ng-template>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="LibraryType.Manga"> <ng-container *ngSwitchCase="LibraryType.Manga">
@ -19,9 +19,9 @@
<ng-template #fullMangaTitle> <ng-template #fullMangaTitle>
{{seriesName.length > 0 ? seriesName + ' - ' : ''}} {{seriesName.length > 0 ? seriesName + ' - ' : ''}}
<ng-container *ngIf="includeVolume && volumeTitle !== ''"> <ng-container *ngIf="includeVolume && volumeTitle !== ''">
{{entity.number !== 0 ? (isChapter && includeVolume ? volumeTitle : '') : ''}} {{entity.minNumber !== 0 ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
</ng-container> </ng-container>
{{entity.number !== 0 ? (isChapter ? (t('chapter') + ' ') + entity.number : volumeTitle) : t('special')}} {{entity.minNumber !== 0 ? (isChapter ? (t('chapter') + ' ') + entity.minNumber : volumeTitle) : t('special')}}
</ng-template> </ng-template>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="LibraryType.Book"> <ng-container *ngSwitchCase="LibraryType.Book">

View File

@ -226,7 +226,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
/** /**
* Track by function for Chapter to tell when to refresh card data * 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}`; 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}`; trackBySeriesIdentify = (index: number, item: Series) => `${item.name}_${item.libraryId}_${item.pagesRead}`;
trackByStoryLineIdentity = (index: number, item: StoryLineItem) => { trackByStoryLineIdentity = (index: number, item: StoryLineItem) => {
@ -341,13 +341,13 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
// This is a lone chapter // This is a lone chapter
if (vol.length === 0) { if (vol.length === 0) {
return 'Ch ' + this.currentlyReadingChapter.number; return 'Ch ' + this.currentlyReadingChapter.minNumber;
} }
if (this.currentlyReadingChapter.number === "0") { if (this.currentlyReadingChapter.minNumber === "0") {
return 'Vol ' + vol[0].number; 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; return this.currentlyReadingChapter.title;

View File

@ -113,9 +113,9 @@ export class DownloadService {
case 'series': case 'series':
return (downloadEntity as Series).name; return (downloadEntity as Series).name;
case 'volume': case 'volume':
return (downloadEntity as Volume).number + ''; return (downloadEntity as Volume).minNumber + '';
case 'chapter': case 'chapter':
return (downloadEntity as Chapter).number; return (downloadEntity as Chapter).minNumber;
case 'bookmark': case 'bookmark':
return ''; return '';
case 'logs': case 'logs':

View File

@ -43,7 +43,7 @@ export class UtilityService {
sortChapters = (a: Chapter, b: Chapter) => { sortChapters = (a: Chapter, b: Chapter) => {
return parseFloat(a.number) - parseFloat(b.number); return parseFloat(a.minNumber) - parseFloat(b.minNumber);
} }
mangaFormatToText(format: MangaFormat): string { mangaFormatToText(format: MangaFormat): string {

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0", "name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
}, },
"version": "0.7.13.6" "version": "0.7.13.7"
}, },
"servers": [ "servers": [
{ {
@ -16338,8 +16338,8 @@
"format": "float" "format": "float"
}, },
"volumeNumber": { "volumeNumber": {
"type": "integer", "type": "number",
"format": "int32" "format": "float"
}, },
"expectedDate": { "expectedDate": {
"type": "string", "type": "string",
@ -20245,7 +20245,18 @@
"number": { "number": {
"type": "integer", "type": "integer",
"description": "The minimum number in the Name field in Int form", "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": { "chapters": {
"type": "array", "type": "array",
@ -20314,9 +20325,13 @@
"type": "integer", "type": "integer",
"format": "int32" "format": "int32"
}, },
"number": { "minNumber": {
"type": "integer", "type": "number",
"format": "int32" "format": "float"
},
"maxNumber": {
"type": "number",
"format": "float"
}, },
"name": { "name": {
"type": "string", "type": "string",