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()
{
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<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();
}

View File

@ -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")

View File

@ -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]

View File

@ -180,7 +180,7 @@ Substitute.For<IMediaConversionService>());
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<IMediaConversionService>());
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<IMediaConversionService>());
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<IMediaConversionService>());
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<IMediaConversionService>());
var series = new SeriesBuilder("Test")
.WithFormat(MangaFormat.Epub)
.WithVolume(new VolumeBuilder("1")
.WithNumber(1)
.WithMinNumber(1)
.WithChapter(new ChapterBuilder("1")
.Build())
.Build())

View File

@ -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();

View File

@ -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

View File

@ -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())

View File

@ -95,7 +95,7 @@
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
<PackageReference Include="SharpCompress" Version="0.36.0" />
<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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@ -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)
{

View File

@ -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)}";
}

View File

@ -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; }
/// <summary>
/// Null if not applicable
/// </summary>

View File

@ -9,9 +9,10 @@ namespace API.DTOs;
public class VolumeDto : IHasReadTimeEstimate
{
public int Id { get; set; }
/// <inheritdoc cref="Volume.Number"/>
public int Number { get; set; }
/// <inheritdoc cref="Volume.MinNumber"/>
public float MinNumber { get; set; }
/// <inheritdoc cref="Volume.MaxNumber"/>
public float MaxNumber { get; set; }
/// <inheritdoc cref="Volume.Name"/>
public string Name { get; set; } = default!;
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")
.HasColumnType("INTEGER");
b.Property<float>("MaxNumber")
.HasColumnType("REAL");
b.Property<int>("MinHoursToRead")
.HasColumnType("INTEGER");
b.Property<float>("MinNumber")
.HasColumnType("REAL");
b.Property<string>("Name")
.HasColumnType("TEXT");

View File

@ -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; }
}

View File

@ -32,7 +32,7 @@ public interface IAppUserProgressRepository
Task<ProgressDto?> GetUserProgressDtoAsync(int chapterId, int userId);
Task<bool> AnyUserProgressForSeriesAsync(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?> 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<int> GetHighestFullyReadVolumeForSeries(int seriesId, int userId)
public async Task<float> 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();
}

View File

@ -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
})

View File

@ -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<VolumeDto>(_mapper.ConfigurationProvider)
.AsNoTracking()
.AsSplitQuery()
@ -215,7 +215,7 @@ public class VolumeRepository : IVolumeRepository
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();
}

View File

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

View File

@ -15,7 +15,17 @@ public class Volume : IEntityDate, IHasReadTimeEstimate
/// <summary>
/// The minimum number in the Name field in Int form
/// </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 DateTime Created { 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>
public static string? GetCoverImage(this Series series)
{
var volumes = (series.Volumes ?? new List<Volume>())
.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);

View File

@ -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);
}
}

View File

@ -15,8 +15,8 @@ public class VolumeBuilder : IEntityBuilder<Volume>
_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<Chapter>()
};
}
@ -27,9 +27,25 @@ public class VolumeBuilder : IEntityBuilder<Volume>
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;
}

View File

@ -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<long?>(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,

View File

@ -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<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
.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<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);
}

View File

@ -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)

View File

@ -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<ChapterDto>();
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),

View File

@ -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,

View File

@ -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<ChapterDto>(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)
};
}

View File

@ -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());
}

View File

@ -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);

View File

@ -10,12 +10,12 @@
</PropertyGroup>
<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="Flurl.Http" Version="3.2.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" 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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@ -7,7 +7,7 @@ import { AgeRating } from './metadata/age-rating';
export interface Chapter {
id: number;
range: string;
number: string;
minNumber: string;
files: Array<MangaFile>;
/**
* 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 {
id: number;
number: number;
minNumber: number;
maxNumber: number;
name: string;
createdUtc: string;
lastModifiedUtc: string;

View File

@ -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();
});

View File

@ -123,7 +123,7 @@
<span>
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions"
[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)}}
</ng-container>
</span>

View File

@ -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<any>, chapter: Chapter) {

View File

@ -7,9 +7,9 @@
<ng-template #fullComicTitle>
{{seriesName.length > 0 ? seriesName + ' - ' : ''}}
<ng-container *ngIf="includeVolume && volumeTitle !== ''">
{{entity.number !== 0 ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
{{entity.minNumber !== 0 ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
</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-container>
<ng-container *ngSwitchCase="LibraryType.Manga">
@ -19,9 +19,9 @@
<ng-template #fullMangaTitle>
{{seriesName.length > 0 ? seriesName + ' - ' : ''}}
<ng-container *ngIf="includeVolume && volumeTitle !== ''">
{{entity.number !== 0 ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
{{entity.minNumber !== 0 ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
</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-container>
<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
*/
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;

View File

@ -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':

View File

@ -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 {

View File

@ -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",