Foundational Rework (#2745)

This commit is contained in:
Joe Milazzo 2024-02-26 14:56:39 -06:00 committed by GitHub
parent 42cd6e9b3a
commit 4fa21fe1ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
92 changed files with 13330 additions and 650 deletions

View file

@ -4,15 +4,16 @@ using Xunit;
namespace API.Tests.Comparers; namespace API.Tests.Comparers;
public class ChapterSortComparerTest public class ChapterSortComparerDefaultLastTest
{ {
[Theory] [Theory]
[InlineData(new[] {1, 2, 0}, new[] {1, 2, 0})] [InlineData(new[] {1, 2, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber}, new[] {1, 2, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber})]
[InlineData(new[] {3, 1, 2}, new[] {1, 2, 3})] [InlineData(new[] {3, 1, 2}, new[] {1, 2, 3})]
[InlineData(new[] {1, 0, 0}, new[] {1, 0, 0})] [InlineData(new[] {1, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber}, new[] {1, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber})]
[InlineData(new[] {API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, 1}, new[] {1, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber})]
public void ChapterSortTest(int[] input, int[] expected) public void ChapterSortTest(int[] input, int[] expected)
{ {
Assert.Equal(expected, input.OrderBy(f => f, new ChapterSortComparer()).ToArray()); Assert.Equal(expected, input.OrderBy(f => f, new ChapterSortComparerDefaultLast()).ToArray());
} }
} }

View file

@ -4,7 +4,7 @@ using Xunit;
namespace API.Tests.Comparers; namespace API.Tests.Comparers;
public class ChapterSortComparerZeroFirstTests public class ChapterSortComparerDefaultFirstTests
{ {
[Theory] [Theory]
[InlineData(new[] {1, 2, 0}, new[] {0, 1, 2,})] [InlineData(new[] {1, 2, 0}, new[] {0, 1, 2,})]
@ -12,13 +12,13 @@ public class ChapterSortComparerZeroFirstTests
[InlineData(new[] {1, 0, 0}, new[] {0, 0, 1})] [InlineData(new[] {1, 0, 0}, new[] {0, 0, 1})]
public void ChapterSortComparerZeroFirstTest(int[] input, int[] expected) public void ChapterSortComparerZeroFirstTest(int[] input, int[] expected)
{ {
Assert.Equal(expected, input.OrderBy(f => f, new ChapterSortComparerZeroFirst()).ToArray()); Assert.Equal(expected, input.OrderBy(f => f, new ChapterSortComparerDefaultFirst()).ToArray());
} }
[Theory] [Theory]
[InlineData(new[] {1.0, 0.5, 0.3}, new[] {0.3, 0.5, 1.0})] [InlineData(new [] {1.0f, 0.5f, 0.3f}, new [] {0.3f, 0.5f, 1.0f})]
public void ChapterSortComparerZeroFirstTest_Doubles(double[] input, double[] expected) public void ChapterSortComparerZeroFirstTest_Doubles(float[] input, float[] expected)
{ {
Assert.Equal(expected, input.OrderBy(f => f, new ChapterSortComparerZeroFirst()).ToArray()); Assert.Equal(expected, input.OrderBy(f => f, new ChapterSortComparerDefaultFirst()).ToArray());
} }
} }

View file

@ -7,11 +7,11 @@ namespace API.Tests.Comparers;
public class SortComparerZeroLastTests public class SortComparerZeroLastTests
{ {
[Theory] [Theory]
[InlineData(new[] {0, 1, 2,}, new[] {1, 2, 0})] [InlineData(new[] {API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, 1, 2,}, new[] {1, 2, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber})]
[InlineData(new[] {3, 1, 2}, new[] {1, 2, 3})] [InlineData(new[] {3, 1, 2}, new[] {1, 2, 3})]
[InlineData(new[] {0, 0, 1}, new[] {1, 0, 0})] [InlineData(new[] {API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, 1}, new[] {1, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapterNumber})]
public void SortComparerZeroLastTest(int[] input, int[] expected) public void SortComparerZeroLastTest(int[] input, int[] expected)
{ {
Assert.Equal(expected, input.OrderBy(f => f, SortComparerZeroLast.Default).ToArray()); Assert.Equal(expected, input.OrderBy(f => f, ChapterSortComparerDefaultLast.Default).ToArray());
} }
} }

View file

@ -17,22 +17,23 @@ public class SeriesExtensionsTests
{ {
var series = new SeriesBuilder("Test 1") var series = new SeriesBuilder("Test 1")
.WithFormat(MangaFormat.Archive) .WithFormat(MangaFormat.Archive)
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithName(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithCoverImage("Special 1") .WithCoverImage("Special 1")
.WithIsSpecial(true) .WithIsSpecial(true)
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1)
.Build()) .Build())
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithCoverImage("Special 2") .WithCoverImage("Special 2")
.WithIsSpecial(true) .WithIsSpecial(true)
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 2)
.Build()) .Build())
.Build()) .Build())
.Build(); .Build();
foreach (var vol in series.Volumes) foreach (var vol in series.Volumes)
{ {
vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), ChapterSortComparerZeroFirst.Default)?.CoverImage; vol.CoverImage = vol.Chapters.MinBy(x => x.MinNumber, ChapterSortComparerDefaultFirst.Default)?.CoverImage;
} }
Assert.Equal("Special 1", series.GetCoverImage()); Assert.Equal("Special 1", series.GetCoverImage());
@ -67,12 +68,36 @@ public class SeriesExtensionsTests
foreach (var vol in series.Volumes) foreach (var vol in series.Volumes)
{ {
vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), ChapterSortComparerZeroFirst.Default)?.CoverImage; vol.CoverImage = vol.Chapters.MinBy(x => x.MinNumber, ChapterSortComparerDefaultFirst.Default)?.CoverImage;
} }
Assert.Equal("Volume 1 Chapter 1", series.GetCoverImage()); Assert.Equal("Volume 1 Chapter 1", series.GetCoverImage());
} }
[Fact]
public void GetCoverImage_LooseChapters_WithSub1_Chapter()
{
var series = new SeriesBuilder("Test 1")
.WithFormat(MangaFormat.Archive)
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.WithName(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.WithChapter(new ChapterBuilder("0.5")
.WithCoverImage("Chapter 0.5")
.Build())
.WithChapter(new ChapterBuilder("2")
.WithCoverImage("Chapter 2")
.Build())
.WithChapter(new ChapterBuilder("1")
.WithCoverImage("Chapter 1")
.Build())
.Build())
.Build();
Assert.Equal("Chapter 1", series.GetCoverImage());
}
[Fact] [Fact]
public void GetCoverImage_JustVolumes() public void GetCoverImage_JustVolumes()
{ {
@ -109,7 +134,7 @@ public class SeriesExtensionsTests
foreach (var vol in series.Volumes) foreach (var vol in series.Volumes)
{ {
vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), ChapterSortComparerZeroFirst.Default)?.CoverImage; vol.CoverImage = vol.Chapters.MinBy(x => x.MinNumber, ChapterSortComparerDefaultFirst.Default)?.CoverImage;
} }
Assert.Equal("Volume 1 Chapter 1", series.GetCoverImage()); Assert.Equal("Volume 1 Chapter 1", series.GetCoverImage());
@ -135,7 +160,7 @@ public class SeriesExtensionsTests
foreach (var vol in series.Volumes) foreach (var vol in series.Volumes)
{ {
vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), ChapterSortComparerZeroFirst.Default)?.CoverImage; vol.CoverImage = vol.Chapters.MinBy(x => x.MinNumber, ChapterSortComparerDefaultFirst.Default)?.CoverImage;
} }
Assert.Equal("Special 2", series.GetCoverImage()); Assert.Equal("Special 2", series.GetCoverImage());
@ -156,16 +181,19 @@ public class SeriesExtensionsTests
.WithIsSpecial(false) .WithIsSpecial(false)
.WithCoverImage("Chapter 2") .WithCoverImage("Chapter 2")
.Build()) .Build())
.Build())
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithIsSpecial(true) .WithIsSpecial(true)
.WithCoverImage("Special 1") .WithCoverImage("Special 1")
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1)
.Build()) .Build())
.Build()) .Build())
.Build(); .Build();
foreach (var vol in series.Volumes) foreach (var vol in series.Volumes)
{ {
vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), ChapterSortComparerZeroFirst.Default)?.CoverImage; vol.CoverImage = vol.Chapters.MinBy(x => x.MinNumber, ChapterSortComparerDefaultFirst.Default)?.CoverImage;
} }
Assert.Equal("Chapter 2", series.GetCoverImage()); Assert.Equal("Chapter 2", series.GetCoverImage());
@ -186,9 +214,12 @@ public class SeriesExtensionsTests
.WithIsSpecial(false) .WithIsSpecial(false)
.WithCoverImage("Chapter 2") .WithCoverImage("Chapter 2")
.Build()) .Build())
.Build())
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithIsSpecial(true) .WithIsSpecial(true)
.WithCoverImage("Special 3") .WithCoverImage("Special 3")
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1)
.Build()) .Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
@ -202,7 +233,7 @@ public class SeriesExtensionsTests
foreach (var vol in series.Volumes) foreach (var vol in series.Volumes)
{ {
vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), ChapterSortComparerZeroFirst.Default)?.CoverImage; vol.CoverImage = vol.Chapters.MinBy(x => x.MinNumber, ChapterSortComparerDefaultFirst.Default)?.CoverImage;
} }
Assert.Equal("Volume 1", series.GetCoverImage()); Assert.Equal("Volume 1", series.GetCoverImage());
@ -223,9 +254,12 @@ public class SeriesExtensionsTests
.WithIsSpecial(false) .WithIsSpecial(false)
.WithCoverImage("Chapter 2") .WithCoverImage("Chapter 2")
.Build()) .Build())
.Build())
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithIsSpecial(true) .WithIsSpecial(true)
.WithCoverImage("Special 1") .WithCoverImage("Special 1")
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1)
.Build()) .Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
@ -239,7 +273,7 @@ public class SeriesExtensionsTests
foreach (var vol in series.Volumes) foreach (var vol in series.Volumes)
{ {
vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), ChapterSortComparerZeroFirst.Default)?.CoverImage; vol.CoverImage = vol.Chapters.MinBy(x => x.MinNumber, ChapterSortComparerDefaultFirst.Default)?.CoverImage;
} }
Assert.Equal("Volume 1", series.GetCoverImage()); Assert.Equal("Volume 1", series.GetCoverImage());
@ -260,9 +294,12 @@ public class SeriesExtensionsTests
.WithIsSpecial(false) .WithIsSpecial(false)
.WithCoverImage("Chapter 1425") .WithCoverImage("Chapter 1425")
.Build()) .Build())
.Build())
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithIsSpecial(true) .WithIsSpecial(true)
.WithCoverImage("Special 1") .WithCoverImage("Special 3")
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1)
.Build()) .Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
@ -283,7 +320,7 @@ public class SeriesExtensionsTests
foreach (var vol in series.Volumes) foreach (var vol in series.Volumes)
{ {
vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), ChapterSortComparerZeroFirst.Default)?.CoverImage; vol.CoverImage = vol.Chapters.MinBy(x => x.MinNumber, ChapterSortComparerDefaultFirst.Default)?.CoverImage;
} }
Assert.Equal("Volume 1", series.GetCoverImage()); Assert.Equal("Volume 1", series.GetCoverImage());
@ -316,7 +353,7 @@ public class SeriesExtensionsTests
foreach (var vol in series.Volumes) foreach (var vol in series.Volumes)
{ {
vol.CoverImage = vol.Chapters.MinBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), ChapterSortComparerZeroFirst.Default)?.CoverImage; vol.CoverImage = vol.Chapters.MinBy(x => x.MinNumber, ChapterSortComparerDefaultFirst.Default)?.CoverImage;
} }
Assert.Equal("Chapter 2", series.GetCoverImage()); Assert.Equal("Chapter 2", series.GetCoverImage());

View file

@ -23,10 +23,41 @@ public class VolumeListExtensionsTests
.Build(), .Build(),
new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithIsSpecial(true).Build()) .Build(),
new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithIsSpecial(true)
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1)
.Build())
.Build(), .Build(),
}; };
var v = volumes.GetCoverImage(MangaFormat.Archive);
Assert.Equal(volumes[0].MinNumber, volumes.GetCoverImage(MangaFormat.Archive).MinNumber);
}
[Fact]
public void GetCoverImage_ChoosesVolume1_WhenHalf()
{
var volumes = new List<Volume>()
{
new VolumeBuilder("1")
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build())
.Build(),
new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.WithChapter(new ChapterBuilder("0.5").Build())
.Build(),
new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithIsSpecial(true)
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1)
.Build())
.Build(),
};
var v = volumes.GetCoverImage(MangaFormat.Archive);
Assert.Equal(volumes[0].MinNumber, volumes.GetCoverImage(MangaFormat.Archive).MinNumber); Assert.Equal(volumes[0].MinNumber, volumes.GetCoverImage(MangaFormat.Archive).MinNumber);
} }
@ -41,7 +72,12 @@ public class VolumeListExtensionsTests
.Build(), .Build(),
new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithIsSpecial(true).Build()) .Build(),
new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithIsSpecial(true)
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1)
.Build())
.Build(), .Build(),
}; };
@ -59,7 +95,12 @@ public class VolumeListExtensionsTests
.Build(), .Build(),
new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithIsSpecial(true).Build()) .Build(),
new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithIsSpecial(true)
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1)
.Build())
.Build(), .Build(),
}; };
@ -77,7 +118,12 @@ public class VolumeListExtensionsTests
.Build(), .Build(),
new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithIsSpecial(true).Build()) .Build(),
new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithIsSpecial(true)
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1)
.Build())
.Build(), .Build(),
}; };
@ -95,7 +141,12 @@ public class VolumeListExtensionsTests
.Build(), .Build(),
new VolumeBuilder("1") new VolumeBuilder("1")
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) .Build(),
new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithIsSpecial(true)
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1)
.Build())
.Build(), .Build(),
}; };

View file

@ -198,7 +198,7 @@ public class DefaultParserTests
filepath = @"E:\Manga\Summer Time Rendering\Specials\Record 014 (between chapter 083 and ch084) SP11.cbr"; filepath = @"E:\Manga\Summer Time Rendering\Specials\Record 014 (between chapter 083 and ch084) SP11.cbr";
expected.Add(filepath, new ParserInfo expected.Add(filepath, new ParserInfo
{ {
Series = "Summer Time Rendering", Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, Edition = "", Series = "Summer Time Rendering", Volumes = API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume, Edition = "",
Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, Filename = "Record 014 (between chapter 083 and ch084) SP11.cbr", Format = MangaFormat.Archive, Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, Filename = "Record 014 (between chapter 083 and ch084) SP11.cbr", Format = MangaFormat.Archive,
FullFilePath = filepath, IsSpecial = true FullFilePath = filepath, IsSpecial = true
}); });
@ -414,7 +414,7 @@ public class DefaultParserTests
filepath = @"E:/Manga/Foo 50/Specials/Foo 50 SP01.cbz"; filepath = @"E:/Manga/Foo 50/Specials/Foo 50 SP01.cbz";
expected = new ParserInfo expected = new ParserInfo
{ {
Series = "Foo 50", Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, IsSpecial = true, Series = "Foo 50", Volumes = API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume, IsSpecial = true,
Chapters = "50", Filename = "Foo 50 SP01.cbz", Format = MangaFormat.Archive, Chapters = "50", Filename = "Foo 50 SP01.cbz", Format = MangaFormat.Archive,
FullFilePath = filepath FullFilePath = filepath
}; };
@ -449,7 +449,7 @@ public class DefaultParserTests
var filepath = @"E:/Comics/Teen Titans/Teen Titans v1 Annual 01 (1967) SP01.cbr"; var filepath = @"E:/Comics/Teen Titans/Teen Titans v1 Annual 01 (1967) SP01.cbr";
expected.Add(filepath, new ParserInfo expected.Add(filepath, new ParserInfo
{ {
Series = "Teen Titans", Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, Series = "Teen Titans", Volumes = API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume,
Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, Filename = "Teen Titans v1 Annual 01 (1967) SP01.cbr", Format = MangaFormat.Archive, Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, Filename = "Teen Titans v1 Annual 01 (1967) SP01.cbr", Format = MangaFormat.Archive,
FullFilePath = filepath FullFilePath = filepath
}); });

View file

@ -294,6 +294,7 @@ public class MangaParserTests
[InlineData("Accel World Volume 2", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] [InlineData("Accel World Volume 2", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)]
[InlineData("Historys Strongest Disciple Kenichi_v11_c90-98", "90-98")] [InlineData("Historys Strongest Disciple Kenichi_v11_c90-98", "90-98")]
[InlineData("Historys Strongest Disciple Kenichi c01-c04", "1-4")] [InlineData("Historys Strongest Disciple Kenichi c01-c04", "1-4")]
[InlineData("Adabana c00-02", "0-2")]
public void ParseChaptersTest(string filename, string expected) public void ParseChaptersTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename)); Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename));

View file

@ -45,6 +45,18 @@ public class ParserTests
Assert.Equal(expected, HasSpecialMarker(input)); Assert.Equal(expected, HasSpecialMarker(input));
} }
[Theory]
[InlineData("Beastars - SP01", 1)]
[InlineData("Beastars SP01", 1)]
[InlineData("Beastars Special 01", 0)]
[InlineData("Beastars Extra 01", 0)]
[InlineData("Batman Beyond - Return of the Joker (2001) SP01", 1)]
[InlineData("Batman Beyond - Return of the Joker (2001)", 0)]
public void ParseSpecialIndexTest(string input, int expected)
{
Assert.Equal(expected, ParseSpecialIndex(input));
}
[Theory] [Theory]
[InlineData("0001", "1")] [InlineData("0001", "1")]
[InlineData("1", "1")] [InlineData("1", "1")]
@ -155,6 +167,7 @@ public class ParserTests
[InlineData("3.5", 3.5)] [InlineData("3.5", 3.5)]
[InlineData("3.5-4.0", 3.5)] [InlineData("3.5-4.0", 3.5)]
[InlineData("asdfasdf", 0.0)] [InlineData("asdfasdf", 0.0)]
[InlineData("-10", -10.0)]
public void MinimumNumberFromRangeTest(string input, float expected) public void MinimumNumberFromRangeTest(string input, float expected)
{ {
Assert.Equal(expected, MinNumberFromRange(input)); Assert.Equal(expected, MinNumberFromRange(input));
@ -171,6 +184,7 @@ public class ParserTests
[InlineData("3.5", 3.5)] [InlineData("3.5", 3.5)]
[InlineData("3.5-4.0", 4.0)] [InlineData("3.5-4.0", 4.0)]
[InlineData("asdfasdf", 0.0)] [InlineData("asdfasdf", 0.0)]
[InlineData("-10", -10.0)]
public void MaximumNumberFromRangeTest(string input, float expected) public void MaximumNumberFromRangeTest(string input, float expected)
{ {
Assert.Equal(expected, MaxNumberFromRange(input)); Assert.Equal(expected, MaxNumberFromRange(input));

View file

@ -395,7 +395,6 @@ public class CleanupServiceTests : AbstractDbTest
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithFormat(MangaFormat.Epub) .WithFormat(MangaFormat.Epub)
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.WithMinNumber(1)
.WithChapter(c) .WithChapter(c)
.Build()) .Build())
.Build(); .Build();

View file

@ -136,7 +136,6 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.WithMinNumber(0)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithPages(1) .WithPages(1)
.Build()) .Build())
@ -166,7 +165,6 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.WithMinNumber(0)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithPages(1) .WithPages(1)
.Build()) .Build())
@ -205,7 +203,6 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.WithMinNumber(0)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithPages(1) .WithPages(1)
.Build()) .Build())
@ -260,7 +257,6 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.WithMinNumber(0)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithPages(1) .WithPages(1)
.Build()) .Build())
@ -299,7 +295,6 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.WithMinNumber(0)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter) .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithPages(1) .WithPages(1)
.Build()) .Build())
@ -347,19 +342,16 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("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")
.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")
.WithMinNumber(3)
.WithChapter(new ChapterBuilder("31").Build()) .WithChapter(new ChapterBuilder("31").Build())
.WithChapter(new ChapterBuilder("32").Build()) .WithChapter(new ChapterBuilder("32").Build())
.Build()) .Build())
@ -379,6 +371,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); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
Assert.NotNull(actualChapter);
Assert.Equal("2", actualChapter.Range); Assert.Equal("2", actualChapter.Range);
} }
@ -390,12 +383,10 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1-2") .WithVolume(new VolumeBuilder("1-2")
.WithMinNumber(1)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("3-4") .WithVolume(new VolumeBuilder("3-4")
.WithMinNumber(2)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.Build()) .Build())
.Build(); .Build();
@ -412,6 +403,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); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
Assert.NotNull(actualChapter);
Assert.Equal("3-4", actualChapter.Volume.Name); Assert.Equal("3-4", actualChapter.Volume.Name);
Assert.Equal("1", actualChapter.Range); Assert.Equal("1", actualChapter.Range);
} }
@ -456,6 +448,7 @@ public class ReaderServiceTests
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 2, 1); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 2, 1);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
Assert.NotNull(actualChapter);
Assert.Equal("31", actualChapter.Range); Assert.Equal("31", actualChapter.Range);
} }
@ -466,19 +459,16 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("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")
.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")
.WithMinNumber(3)
.WithChapter(new ChapterBuilder("31").Build()) .WithChapter(new ChapterBuilder("31").Build())
.WithChapter(new ChapterBuilder("32").Build()) .WithChapter(new ChapterBuilder("32").Build())
.Build()) .Build())
@ -497,6 +487,7 @@ public class ReaderServiceTests
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
Assert.NotNull(actualChapter);
Assert.Equal("21", actualChapter.Range); Assert.Equal("21", actualChapter.Range);
} }
@ -507,19 +498,16 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("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")
.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")
.WithMinNumber(3)
.WithChapter(new ChapterBuilder("31").Build()) .WithChapter(new ChapterBuilder("31").Build())
.WithChapter(new ChapterBuilder("32").Build()) .WithChapter(new ChapterBuilder("32").Build())
.Build()) .Build())
@ -539,6 +527,7 @@ public class ReaderServiceTests
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
Assert.NotNull(actualChapter);
Assert.Equal("21", actualChapter.Range); Assert.Equal("21", actualChapter.Range);
} }
@ -549,15 +538,13 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.WithMinNumber(0) .WithChapter(new ChapterBuilder("21").Build())
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("22").Build())
.WithChapter(new ChapterBuilder("2").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("2").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();
@ -574,7 +561,8 @@ public class ReaderServiceTests
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 4, 1); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 4, 1);
Assert.NotEqual(-1, nextChapter); Assert.NotEqual(-1, nextChapter);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
Assert.Equal("1", actualChapter.Range); Assert.NotNull(actualChapter);
Assert.Equal("21", actualChapter.Range);
} }
[Fact] [Fact]
@ -584,18 +572,15 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.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")
.WithMinNumber(1)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("2") .WithVolume(new VolumeBuilder("2")
.WithMinNumber(2)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build())
.Build()) .Build())
.Build(); .Build();
@ -616,6 +601,7 @@ public class ReaderServiceTests
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 3, 1); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 3, 1);
Assert.NotEqual(-1, nextChapter); Assert.NotEqual(-1, nextChapter);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
Assert.NotNull(actualChapter);
Assert.Equal(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, actualChapter.Range); Assert.Equal(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, actualChapter.Range);
} }
@ -626,15 +612,13 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("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(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithMinNumber(0) .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber).Build())
.WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).Build())
.WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build())
.Build()) .Build())
.Build(); .Build();
series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
@ -658,7 +642,6 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("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())
@ -684,7 +667,6 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.WithMinNumber(0)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.Build()) .Build())
@ -704,68 +686,69 @@ public class ReaderServiceTests
} }
// This is commented out because, while valid, I can't solve how to make this pass (https://github.com/Kareadita/Kavita/issues/2099) // This is commented out because, while valid, I can't solve how to make this pass (https://github.com/Kareadita/Kavita/issues/2099)
// [Fact] [Fact]
// public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromLastChapter_NoSpecials_FirstIsVolume() public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromLastChapter_NoSpecials_FirstIsVolume()
// { {
// await ResetDb(); await ResetDb();
//
// var series = new SeriesBuilder("Test")
// .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume)
// .WithMinNumber(0)
// .WithChapter(new ChapterBuilder("1").Build())
// .WithChapter(new ChapterBuilder("2").Build())
// .Build())
// .WithVolume(new VolumeBuilder("1")
// .WithMinNumber(1)
// .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build())
// .Build())
// .Build();
// series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
//
// _context.Series.Add(series);
// _context.AppUser.Add(new AppUser()
// {
// UserName = "majora2007"
// });
//
// await _context.SaveChangesAsync();
//
// var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 3, 1);
// Assert.Equal(-1, nextChapter);
// }
// This is commented out because, while valid, I can't solve how to make this pass var series = new SeriesBuilder("Test")
// [Fact] .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
// public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromLastChapter_WithSpecials() .WithChapter(new ChapterBuilder("1").Build())
// { .WithChapter(new ChapterBuilder("2").Build())
// await ResetDb(); .Build())
// .WithVolume(new VolumeBuilder("1")
// var series = new SeriesBuilder("Test") .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build())
// .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultVolume) .Build())
// .WithMinNumber(0) .Build();
// .WithChapter(new ChapterBuilder("1").Build()) series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
// .WithChapter(new ChapterBuilder("2").Build())
// .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithIsSpecial(true).Build()) _context.Series.Add(series);
// .Build()) _context.AppUser.Add(new AppUser()
// {
// .WithVolume(new VolumeBuilder("1") UserName = "majora2007"
// .WithMinNumber(1) });
// .WithChapter(new ChapterBuilder("2").Build())
// .Build()) await _context.SaveChangesAsync();
// .Build();
// series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1);
// Assert.Equal(-1, nextChapter);
// _context.Series.Add(series); }
// _context.AppUser.Add(new AppUser()
// { [Fact]
// UserName = "majora2007" public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromLastChapter_WithSpecials()
// }); {
// await ResetDb();
// await _context.SaveChangesAsync();
// var series = new SeriesBuilder("Test")
// var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 4, 1); .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
// Assert.Equal(-1, nextChapter); .WithChapter(new ChapterBuilder("1").Build())
// } .WithChapter(new ChapterBuilder("2").Build())
.Build())
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithIsSpecial(true)
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1)
.Build())
.Build())
.WithVolume(new VolumeBuilder("1")
.WithChapter(new ChapterBuilder("2").Build())
.Build())
.Build();
series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
_context.Series.Add(series);
_context.AppUser.Add(new AppUser()
{
UserName = "majora2007"
});
await _context.SaveChangesAsync();
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 3, 1);
Assert.Equal(-1, nextChapter);
}
@ -776,15 +759,19 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("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(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithMinNumber(0) .WithChapter(new ChapterBuilder("A.cbz")
.WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) .WithIsSpecial(true)
.WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1)
.Build())
.WithChapter(new ChapterBuilder("B.cbz")
.WithIsSpecial(true)
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 2)
.Build())
.Build()) .Build())
.Build(); .Build();
series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
@ -802,6 +789,7 @@ public class ReaderServiceTests
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1);
Assert.NotEqual(-1, nextChapter); Assert.NotEqual(-1, nextChapter);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
Assert.NotNull(actualChapter);
Assert.Equal("A.cbz", actualChapter.Range); Assert.Equal("A.cbz", actualChapter.Range);
} }
@ -812,10 +800,16 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.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()) .Build())
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithChapter(new ChapterBuilder("A.cbz")
.WithIsSpecial(true)
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1)
.WithPages(1)
.Build())
.Build()) .Build())
.Build(); .Build();
series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
@ -833,6 +827,7 @@ public class ReaderServiceTests
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1);
Assert.NotEqual(-1, nextChapter); Assert.NotEqual(-1, nextChapter);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
Assert.NotNull(actualChapter);
Assert.Equal("A.cbz", actualChapter.Range); Assert.Equal("A.cbz", actualChapter.Range);
} }
@ -843,15 +838,21 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.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())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithMinNumber(1)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithChapter(new ChapterBuilder("A.cbz")
.WithIsSpecial(true)
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1)
.WithPages(1)
.Build())
.Build())
.Build(); .Build();
series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
@ -864,7 +865,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 3, 1); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 3, 4, 1);
Assert.Equal(-1, nextChapter); Assert.Equal(-1, nextChapter);
} }
@ -876,14 +877,18 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("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(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithMinNumber(0) .WithChapter(new ChapterBuilder("A.cbz")
.WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) .WithIsSpecial(true)
.WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1)
.Build())
.WithChapter(new ChapterBuilder("B.cbz")
.WithIsSpecial(true)
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 2)
.Build())
.Build()) .Build())
.Build(); .Build();
series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
@ -901,6 +906,7 @@ public class ReaderServiceTests
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 3, 1); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 3, 1);
Assert.NotEqual(-1, nextChapter); Assert.NotEqual(-1, nextChapter);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
Assert.NotNull(actualChapter);
Assert.Equal("B.cbz", actualChapter.Range); Assert.Equal("B.cbz", actualChapter.Range);
} }
@ -911,12 +917,10 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithMinNumber(1)
.WithChapter(new ChapterBuilder("12").Build()) .WithChapter(new ChapterBuilder("12").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("2") .WithVolume(new VolumeBuilder("2")
.WithMinNumber(2)
.WithChapter(new ChapterBuilder("12").Build()) .WithChapter(new ChapterBuilder("12").Build())
.Build()) .Build())
.Build(); .Build();
@ -952,19 +956,16 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("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")
.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")
.WithMinNumber(3)
.WithChapter(new ChapterBuilder("31").Build()) .WithChapter(new ChapterBuilder("31").Build())
.WithChapter(new ChapterBuilder("32").Build()) .WithChapter(new ChapterBuilder("32").Build())
.Build()) .Build())
@ -984,6 +985,7 @@ public class ReaderServiceTests
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 1, 2, 1); var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 1, 2, 1);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
Assert.NotNull(actualChapter);
Assert.Equal("1", actualChapter.Range); Assert.Equal("1", actualChapter.Range);
} }
@ -995,19 +997,16 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("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")
.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")
.WithMinNumber(3)
.WithChapter(new ChapterBuilder("31").Build()) .WithChapter(new ChapterBuilder("31").Build())
.WithChapter(new ChapterBuilder("32").Build()) .WithChapter(new ChapterBuilder("32").Build())
.Build()) .Build())
@ -1025,6 +1024,7 @@ public class ReaderServiceTests
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 3, 5, 1); var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 3, 5, 1);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
Assert.NotNull(actualChapter);
Assert.Equal("22", actualChapter.Range); Assert.Equal("22", actualChapter.Range);
} }
@ -1038,7 +1038,14 @@ public class ReaderServiceTests
.WithChapter(new ChapterBuilder("40").WithPages(1).Build()) .WithChapter(new ChapterBuilder("40").WithPages(1).Build())
.WithChapter(new ChapterBuilder("50").WithPages(1).Build()) .WithChapter(new ChapterBuilder("50").WithPages(1).Build())
.WithChapter(new ChapterBuilder("60").WithPages(1).Build()) .WithChapter(new ChapterBuilder("60").WithPages(1).Build())
.WithChapter(new ChapterBuilder("Some Special Title").WithPages(1).WithIsSpecial(true).Build()) .Build())
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithChapter(new ChapterBuilder("Some Special Title")
.WithIsSpecial(true)
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1)
.WithPages(1)
.Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1997") .WithVolume(new VolumeBuilder("1997")
@ -1065,7 +1072,7 @@ public class ReaderServiceTests
// prevChapter should be id from ch.21 from volume 2001 // prevChapter should be id from ch.21 from volume 2001
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 4, 7, 1); var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 5, 7, 1);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
Assert.NotNull(actualChapter); Assert.NotNull(actualChapter);
@ -1109,6 +1116,7 @@ public class ReaderServiceTests
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 3, 1); var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 3, 1);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
Assert.NotNull(actualChapter);
Assert.Equal("2", actualChapter.Range); Assert.Equal("2", actualChapter.Range);
} }
@ -1119,15 +1127,13 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("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(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithMinNumber(0) .WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).Build())
.WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 2).Build())
.WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build())
.Build()) .Build())
.Build(); .Build();
series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
@ -1147,6 +1153,7 @@ public class ReaderServiceTests
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 3, 1); var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 3, 1);
Assert.Equal(2, prevChapter); Assert.Equal(2, prevChapter);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
Assert.NotNull(actualChapter);
Assert.Equal("2", actualChapter.Range); Assert.Equal("2", actualChapter.Range);
} }
@ -1157,7 +1164,6 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("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())
@ -1187,7 +1193,6 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithMinNumber(1)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build())
.Build()) .Build())
.Build(); .Build();
@ -1216,13 +1221,11 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.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")
.WithMinNumber(1)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build()) .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build())
.Build()) .Build())
.Build(); .Build();
@ -1237,10 +1240,7 @@ public class ReaderServiceTests
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 3, 1);
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
Assert.Equal(-1, prevChapter); Assert.Equal(-1, prevChapter);
} }
@ -1251,22 +1251,19 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.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")
.WithMinNumber(1) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("1").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("2").Build())
.WithChapter(new ChapterBuilder("2").WithIsSpecial(true).Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("2") .WithVolume(new VolumeBuilder("2")
.WithMinNumber(2) .WithChapter(new ChapterBuilder("3").Build())
.WithChapter(new ChapterBuilder("3").WithIsSpecial(true).Build()) .WithChapter(new ChapterBuilder("4").Build())
.WithChapter(new ChapterBuilder("4").WithIsSpecial(true).Build())
.Build()) .Build())
.Build(); .Build();
series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
@ -1299,7 +1296,6 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.WithMinNumber(0)
.WithChapter(new ChapterBuilder("1").Build()) .WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build()) .WithChapter(new ChapterBuilder("2").Build())
.Build()) .Build())
@ -1329,14 +1325,18 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("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(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithMinNumber(0) .WithChapter(new ChapterBuilder("A.cbz")
.WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build()) .WithIsSpecial(true)
.WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).Build()) .WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1)
.Build())
.WithChapter(new ChapterBuilder("B.cbz")
.WithIsSpecial(true)
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 2)
.Build())
.Build()) .Build())
.Build(); .Build();
series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
@ -1357,6 +1357,7 @@ public class ReaderServiceTests
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 4, 1); var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 4, 1);
Assert.NotEqual(-1, prevChapter); Assert.NotEqual(-1, prevChapter);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
Assert.NotNull(actualChapter);
Assert.Equal("A.cbz", actualChapter.Range); Assert.Equal("A.cbz", actualChapter.Range);
} }
@ -1367,12 +1368,10 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.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")
.WithMinNumber(1)
.WithChapter(new ChapterBuilder("21").Build()) .WithChapter(new ChapterBuilder("21").Build())
.WithChapter(new ChapterBuilder("22").Build()) .WithChapter(new ChapterBuilder("22").Build())
.Build()) .Build())
@ -1389,12 +1388,10 @@ public class ReaderServiceTests
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 1, 1, 1); var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
Assert.NotEqual(-1, prevChapter); Assert.NotEqual(-1, prevChapter);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
Assert.NotNull(actualChapter);
Assert.Equal("22", actualChapter.Range); Assert.Equal("22", actualChapter.Range);
} }
@ -1405,12 +1402,10 @@ public class ReaderServiceTests
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithMinNumber(1)
.WithChapter(new ChapterBuilder("12").Build()) .WithChapter(new ChapterBuilder("12").Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("2") .WithVolume(new VolumeBuilder("2")
.WithMinNumber(2)
.WithChapter(new ChapterBuilder("12").Build()) .WithChapter(new ChapterBuilder("12").Build())
.Build()) .Build())
.Build(); .Build();
@ -1630,7 +1625,12 @@ public class ReaderServiceTests
.WithChapter(new ChapterBuilder("46").WithPages(1).Build()) .WithChapter(new ChapterBuilder("46").WithPages(1).Build())
.WithChapter(new ChapterBuilder("47").WithPages(1).Build()) .WithChapter(new ChapterBuilder("47").WithPages(1).Build())
.WithChapter(new ChapterBuilder("48").WithPages(1).Build()) .WithChapter(new ChapterBuilder("48").WithPages(1).Build())
.WithChapter(new ChapterBuilder("Some Special Title").WithIsSpecial(true).WithPages(1).Build()) .Build())
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithChapter(new ChapterBuilder("Some Special Title")
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1)
.WithIsSpecial(true).WithPages(1)
.Build())
.Build()) .Build())
// One file volume // One file volume
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
@ -1697,7 +1697,9 @@ public class ReaderServiceTests
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.WithChapter(new ChapterBuilder("1").WithPages(1).Build()) .WithChapter(new ChapterBuilder("1").WithPages(1).Build())
.WithChapter(new ChapterBuilder("2").WithPages(1).Build()) .WithChapter(new ChapterBuilder("2").WithPages(1).Build())
.WithChapter(new ChapterBuilder("Prologue").WithIsSpecial(true).WithPages(1).Build()) .Build())
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithChapter(new ChapterBuilder("Prologue").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithPages(1).Build())
.Build()) .Build())
.Build(); .Build();
series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
@ -1821,7 +1823,9 @@ public class ReaderServiceTests
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.WithChapter(new ChapterBuilder("100").WithPages(1).Build()) .WithChapter(new ChapterBuilder("100").WithPages(1).Build())
.WithChapter(new ChapterBuilder("101").WithPages(1).Build()) .WithChapter(new ChapterBuilder("101").WithPages(1).Build())
.WithChapter(new ChapterBuilder("Christmas Eve").WithIsSpecial(true).WithPages(1).Build()) .Build())
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithChapter(new ChapterBuilder("Christmas Eve").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithPages(1).Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
@ -2031,7 +2035,9 @@ public class ReaderServiceTests
.WithChapter(new ChapterBuilder("1").WithPages(1).Build()) .WithChapter(new ChapterBuilder("1").WithPages(1).Build())
.WithChapter(new ChapterBuilder("2").WithPages(1).Build()) .WithChapter(new ChapterBuilder("2").WithPages(1).Build())
.WithChapter(new ChapterBuilder("3").WithPages(1).Build()) .WithChapter(new ChapterBuilder("3").WithPages(1).Build())
.WithChapter(new ChapterBuilder("Some Special Title").WithIsSpecial(true).WithPages(1).Build()) .Build())
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithChapter(new ChapterBuilder("Some Special Title").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithPages(1).Build())
.Build()) .Build())
.Build(); .Build();
series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
@ -2211,7 +2217,9 @@ public class ReaderServiceTests
.WithChapter(new ChapterBuilder("51").WithPages(1).Build()) .WithChapter(new ChapterBuilder("51").WithPages(1).Build())
.WithChapter(new ChapterBuilder("52").WithPages(1).Build()) .WithChapter(new ChapterBuilder("52").WithPages(1).Build())
.WithChapter(new ChapterBuilder("91").WithPages(2).Build()) .WithChapter(new ChapterBuilder("91").WithPages(2).Build())
.WithChapter(new ChapterBuilder("Special").WithIsSpecial(true).WithPages(1).Build()) .Build())
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithChapter(new ChapterBuilder("Special").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithPages(1).Build())
.Build()) .Build())
.Build(); .Build();
series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
@ -2380,7 +2388,9 @@ public class ReaderServiceTests
.WithChapter(new ChapterBuilder("1").WithPages(1).Build()) .WithChapter(new ChapterBuilder("1").WithPages(1).Build())
.WithChapter(new ChapterBuilder("2").WithPages(1).Build()) .WithChapter(new ChapterBuilder("2").WithPages(1).Build())
.WithChapter(new ChapterBuilder("3").WithPages(1).Build()) .WithChapter(new ChapterBuilder("3").WithPages(1).Build())
.WithChapter(new ChapterBuilder("Some Special Title").WithIsSpecial(true).WithPages(1).Build()) .Build())
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(1).Build())
.Build()) .Build())
.Build(); .Build();
series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
@ -2418,8 +2428,10 @@ public class ReaderServiceTests
.WithChapter(new ChapterBuilder("2").WithPages(1).Build()) .WithChapter(new ChapterBuilder("2").WithPages(1).Build())
.WithChapter(new ChapterBuilder("2.5").WithPages(1).Build()) .WithChapter(new ChapterBuilder("2.5").WithPages(1).Build())
.WithChapter(new ChapterBuilder("3").WithPages(1).Build()) .WithChapter(new ChapterBuilder("3").WithPages(1).Build())
.WithChapter(new ChapterBuilder("Some Special Title").WithIsSpecial(true).WithPages(1).Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(1).Build())
.Build())
.Build(); .Build();
series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
@ -2493,7 +2505,9 @@ public class ReaderServiceTests
.WithChapter(new ChapterBuilder("48").WithPages(48).Build()) .WithChapter(new ChapterBuilder("48").WithPages(48).Build())
.WithChapter(new ChapterBuilder("49").WithPages(49).Build()) .WithChapter(new ChapterBuilder("49").WithPages(49).Build())
.WithChapter(new ChapterBuilder("50").WithPages(50).Build()) .WithChapter(new ChapterBuilder("50").WithPages(50).Build())
.WithChapter(new ChapterBuilder("Some Special Title").WithIsSpecial(true).WithPages(10).Build()) .Build())
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(10).Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
@ -2550,14 +2564,14 @@ public class ReaderServiceTests
public async Task MarkSeriesAsReadTest() public async Task MarkSeriesAsReadTest()
{ {
await ResetDb(); await ResetDb();
// TODO: Validate this is correct, shouldn't be possible to have 2 Volume 0's in a series
var series = new SeriesBuilder("Test") var series = new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder("1")
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build())
.WithChapter(new ChapterBuilder("1").WithPages(2).Build()) .WithChapter(new ChapterBuilder("1").WithPages(2).Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder("2")
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build())
.WithChapter(new ChapterBuilder("1").WithPages(2).Build()) .WithChapter(new ChapterBuilder("1").WithPages(2).Build())
.Build()) .Build())
@ -2669,7 +2683,9 @@ public class ReaderServiceTests
.WithChapter(new ChapterBuilder("10").WithPages(1).Build()) .WithChapter(new ChapterBuilder("10").WithPages(1).Build())
.WithChapter(new ChapterBuilder("20").WithPages(1).Build()) .WithChapter(new ChapterBuilder("20").WithPages(1).Build())
.WithChapter(new ChapterBuilder("30").WithPages(1).Build()) .WithChapter(new ChapterBuilder("30").WithPages(1).Build())
.WithChapter(new ChapterBuilder("Some Special Title").WithIsSpecial(true).WithPages(1).Build()) .Build())
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(1).Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1997") .WithVolume(new VolumeBuilder("1997")
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build()) .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithPages(1).Build())
@ -2722,7 +2738,9 @@ public class ReaderServiceTests
.WithChapter(new ChapterBuilder("10").WithPages(1).Build()) .WithChapter(new ChapterBuilder("10").WithPages(1).Build())
.WithChapter(new ChapterBuilder("20").WithPages(1).Build()) .WithChapter(new ChapterBuilder("20").WithPages(1).Build())
.WithChapter(new ChapterBuilder("30").WithPages(1).Build()) .WithChapter(new ChapterBuilder("30").WithPages(1).Build())
.WithChapter(new ChapterBuilder("Some Special Title").WithIsSpecial(true).WithPages(1).Build()) .Build())
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithChapter(new ChapterBuilder("Some Special Title").WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithIsSpecial(true).WithPages(1).Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1997") .WithVolume(new VolumeBuilder("1997")
.WithChapter(new ChapterBuilder("1").WithPages(1).Build()) .WithChapter(new ChapterBuilder("1").WithPages(1).Build())

View file

@ -108,9 +108,9 @@ public class SeriesServiceTests : AbstractDbTest
.WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build())
.WithSeries(new SeriesBuilder("Test") .WithSeries(new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithChapter(new ChapterBuilder("Omake").WithIsSpecial(true).WithTitle("Omake").WithPages(1).Build()) .WithChapter(new ChapterBuilder("Omake").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithTitle("Omake").WithPages(1).Build())
.WithChapter(new ChapterBuilder("Something SP02").WithIsSpecial(true).WithTitle("Something").WithPages(1).Build()) .WithChapter(new ChapterBuilder("Something SP02").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 2).WithTitle("Something").WithPages(1).Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("2") .WithVolume(new VolumeBuilder("2")
.WithChapter(new ChapterBuilder("21").WithPages(1).Build()) .WithChapter(new ChapterBuilder("21").WithPages(1).Build())
@ -280,11 +280,13 @@ public class SeriesServiceTests : AbstractDbTest
.WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build())
.WithSeries(new SeriesBuilder("Test") .WithSeries(new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithChapter(new ChapterBuilder("Ano Orokamono ni mo Kyakkou wo! - Volume 1.epub", "Ano Orokamono ni mo Kyakkou wo! - Volume 1.epub").WithIsSpecial(true).WithPages(1).Build()) .WithChapter(new ChapterBuilder("Ano Orokamono ni mo Kyakkou wo! - Volume 1.epub", "Ano Orokamono ni mo Kyakkou wo! - Volume 1.epub")
.WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithPages(1).Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("2") .WithVolume(new VolumeBuilder("2")
.WithChapter(new ChapterBuilder("Ano Orokamono ni mo Kyakkou wo! - Volume 2.epub", "Ano Orokamono ni mo Kyakkou wo! - Volume 2.epub").WithPages(1).Build()) .WithChapter(new ChapterBuilder("Ano Orokamono ni mo Kyakkou wo! - Volume 2.epub", "Ano Orokamono ni mo Kyakkou wo! - Volume 2.epub")
.WithPages(1).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).Build())
.Build()) .Build())
.Build()) .Build())
.Build()); .Build());
@ -779,7 +781,9 @@ public class SeriesServiceTests : AbstractDbTest
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume) .WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.WithChapter(new ChapterBuilder("95").WithPages(1).WithFile(file).Build()) .WithChapter(new ChapterBuilder("95").WithPages(1).WithFile(file).Build())
.WithChapter(new ChapterBuilder("96").WithPages(1).WithFile(file).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).WithFile(file).Build())
.WithChapter(new ChapterBuilder("A Special Case").WithIsSpecial(true).WithFile(file).WithPages(1).Build()) .Build())
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
.WithChapter(new ChapterBuilder("A Special Case").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithFile(file).WithPages(1).Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithChapter(new ChapterBuilder("1").WithPages(1).WithFile(file).Build()) .WithChapter(new ChapterBuilder("1").WithPages(1).WithFile(file).Build())
@ -829,6 +833,7 @@ public class SeriesServiceTests : AbstractDbTest
var firstChapter = SeriesService.GetFirstChapterForMetadata(series); var firstChapter = SeriesService.GetFirstChapterForMetadata(series);
Assert.NotNull(firstChapter); Assert.NotNull(firstChapter);
Assert.NotNull(firstChapter);
Assert.Same("1", firstChapter.Range); Assert.Same("1", firstChapter.Range);
} }
@ -838,7 +843,8 @@ public class SeriesServiceTests : AbstractDbTest
var series = CreateSeriesMock(); var series = CreateSeriesMock();
var firstChapter = SeriesService.GetFirstChapterForMetadata(series); var firstChapter = SeriesService.GetFirstChapterForMetadata(series);
Assert.Same("1", firstChapter.Range); Assert.NotNull(firstChapter);
Assert.Equal(1, firstChapter.MinNumber);
} }
[Fact] [Fact]
@ -849,7 +855,7 @@ public class SeriesServiceTests : AbstractDbTest
{ {
new MangaFileBuilder("Test.cbz", MangaFormat.Archive, 1).Build() new MangaFileBuilder("Test.cbz", MangaFormat.Archive, 1).Build()
}; };
series.Volumes[1].Chapters = new List<Chapter>() series.Volumes[2].Chapters = new List<Chapter>()
{ {
new ChapterBuilder("2").WithFiles(files).WithPages(1).Build(), new ChapterBuilder("2").WithFiles(files).WithPages(1).Build(),
new ChapterBuilder("1.1").WithFiles(files).WithPages(1).Build(), new ChapterBuilder("1.1").WithFiles(files).WithPages(1).Build(),
@ -857,7 +863,8 @@ public class SeriesServiceTests : AbstractDbTest
}; };
var firstChapter = SeriesService.GetFirstChapterForMetadata(series); var firstChapter = SeriesService.GetFirstChapterForMetadata(series);
Assert.Same("1.1", firstChapter.Range); Assert.NotNull(firstChapter);
Assert.True(firstChapter.MinNumber.Is(1.1f));
} }
[Fact] [Fact]
@ -882,7 +889,8 @@ public class SeriesServiceTests : AbstractDbTest
series.Library = new LibraryBuilder("Test LIb", LibraryType.Book).Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Book).Build();
var firstChapter = SeriesService.GetFirstChapterForMetadata(series); var firstChapter = SeriesService.GetFirstChapterForMetadata(series);
Assert.Same("1", firstChapter.Range); Assert.NotNull(firstChapter);
Assert.Equal(1, firstChapter.MinNumber);
} }
#endregion #endregion
@ -919,6 +927,7 @@ public class SeriesServiceTests : AbstractDbTest
addRelationDto.Adaptations.Add(2); addRelationDto.Adaptations.Add(2);
addRelationDto.Sequels.Add(3); addRelationDto.Sequels.Add(3);
await _seriesService.UpdateRelatedSeries(addRelationDto); await _seriesService.UpdateRelatedSeries(addRelationDto);
Assert.NotNull(series1);
Assert.Equal(2, series1.Relations.Single(s => s.TargetSeriesId == 2).TargetSeriesId); Assert.Equal(2, series1.Relations.Single(s => s.TargetSeriesId == 2).TargetSeriesId);
Assert.Equal(3, series1.Relations.Single(s => s.TargetSeriesId == 3).TargetSeriesId); Assert.Equal(3, series1.Relations.Single(s => s.TargetSeriesId == 3).TargetSeriesId);
} }
@ -1291,7 +1300,7 @@ public class SeriesServiceTests : AbstractDbTest
.Build()); .Build());
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(true).Build(); var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).Build();
Assert.Equal("Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Manga, false)); Assert.Equal("Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Manga, false));
} }
@ -1323,7 +1332,7 @@ public class SeriesServiceTests : AbstractDbTest
.Build()); .Build());
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(true).Build(); var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).Build();
Assert.Equal("Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Comic, false)); Assert.Equal("Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Comic, false));
} }
@ -1355,7 +1364,7 @@ public class SeriesServiceTests : AbstractDbTest
.Build()); .Build());
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(true).Build(); var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).Build();
Assert.Equal("Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Comic, true)); Assert.Equal("Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Comic, true));
} }
@ -1387,7 +1396,7 @@ public class SeriesServiceTests : AbstractDbTest
.Build()); .Build());
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(true).Build(); var chapter = new ChapterBuilder("1").WithTitle("Some title").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).Build();
Assert.Equal("Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Book, false)); Assert.Equal("Some title", await _seriesService.FormatChapterTitle(1, chapter, LibraryType.Book, false));
} }
@ -1484,4 +1493,116 @@ public class SeriesServiceTests : AbstractDbTest
} }
#endregion #endregion
#region GetEstimatedChapterCreationDate
[Fact]
public async Task GetEstimatedChapterCreationDate_NoNextChapter_InvalidType()
{
await ResetDb();
_context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book)
.WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build())
.WithSeries(new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.WithChapter(new ChapterBuilder("1").WithPages(1).Build())
.WithChapter(new ChapterBuilder("2").WithPages(1).Build())
.WithChapter(new ChapterBuilder("3").WithPages(1).Build())
.Build())
.Build())
.Build());
await _context.SaveChangesAsync();
var nextChapter = await _seriesService.GetEstimatedChapterCreationDate(1, 1);
Assert.Equal(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolumeNumber, nextChapter.VolumeNumber);
Assert.Equal(0, nextChapter.ChapterNumber);
}
[Fact]
public async Task GetEstimatedChapterCreationDate_NoNextChapter_InvalidPublicationStatus()
{
await ResetDb();
_context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Manga)
.WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build())
.WithSeries(new SeriesBuilder("Test")
.WithPublicationStatus(PublicationStatus.Completed)
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.WithChapter(new ChapterBuilder("1").WithPages(1).Build())
.WithChapter(new ChapterBuilder("2").WithPages(1).Build())
.WithChapter(new ChapterBuilder("3").WithPages(1).Build())
.Build())
.Build())
.Build());
await _context.SaveChangesAsync();
var nextChapter = await _seriesService.GetEstimatedChapterCreationDate(1, 1);
Assert.Equal(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolumeNumber, nextChapter.VolumeNumber);
Assert.Equal(0, nextChapter.ChapterNumber);
}
[Fact]
public async Task GetEstimatedChapterCreationDate_NoNextChapter_Only2Chapters()
{
await ResetDb();
_context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book)
.WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build())
.WithSeries(new SeriesBuilder("Test")
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.WithChapter(new ChapterBuilder("1").WithPages(1).Build())
.WithChapter(new ChapterBuilder("2").WithPages(1).Build())
.Build())
.Build())
.Build());
await _context.SaveChangesAsync();
var nextChapter = await _seriesService.GetEstimatedChapterCreationDate(1, 1);
Assert.NotNull(nextChapter);
Assert.Equal(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolumeNumber, nextChapter.VolumeNumber);
Assert.Equal(0, nextChapter.ChapterNumber);
}
[Fact]
public async Task GetEstimatedChapterCreationDate_NextChapter_ChaptersMonthApart()
{
await ResetDb();
var now = DateTime.UtcNow;
_context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Manga)
.WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build())
.WithSeries(new SeriesBuilder("Test")
.WithPublicationStatus(PublicationStatus.OnGoing)
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.WithChapter(new ChapterBuilder("1").WithCreated(now).WithPages(1).Build())
.WithChapter(new ChapterBuilder("2").WithCreated(now.AddMonths(1)).WithPages(1).Build())
.WithChapter(new ChapterBuilder("3").WithCreated(now.AddMonths(2)).WithPages(1).Build())
.WithChapter(new ChapterBuilder("4").WithCreated(now.AddMonths(3)).WithPages(1).Build())
.Build())
.Build())
.Build());
await _context.SaveChangesAsync();
var nextChapter = await _seriesService.GetEstimatedChapterCreationDate(1, 1);
Assert.NotNull(nextChapter);
Assert.Equal(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolumeNumber, nextChapter.VolumeNumber);
Assert.Equal(5, nextChapter.ChapterNumber);
Assert.NotNull(nextChapter.ExpectedDate);
var expected = now.AddMonths(4);
Assert.Equal(expected.Month, nextChapter.ExpectedDate.Value.Month);
Assert.True(nextChapter.ExpectedDate.Value.Day >= expected.Day - 1 || nextChapter.ExpectedDate.Value.Day <= expected.Day + 1);
}
#endregion
} }

View file

@ -130,7 +130,7 @@ public class TachiyomiServiceTests
.WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build()) .WithChapter(new ChapterBuilder("1").WithPages(1).Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("2") .WithVolume(new VolumeBuilder("2")
.WithChapter(new ChapterBuilder("3").WithPages(1).Build()) .WithChapter(new ChapterBuilder("3").WithPages(1).Build())
@ -175,7 +175,7 @@ public class TachiyomiServiceTests
.WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build()) .WithChapter(new ChapterBuilder("1").WithPages(1).Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("2") .WithVolume(new VolumeBuilder("2")
.WithChapter(new ChapterBuilder("3").WithPages(1).Build()) .WithChapter(new ChapterBuilder("3").WithPages(1).Build())
@ -265,6 +265,7 @@ public class TachiyomiServiceTests
Assert.Equal("21", latestChapter.Number); Assert.Equal("21", latestChapter.Number);
} }
[Fact] [Fact]
public async Task GetLatestChapter_ShouldReturnEncodedVolume_Progress() public async Task GetLatestChapter_ShouldReturnEncodedVolume_Progress()
{ {
@ -276,7 +277,7 @@ public class TachiyomiServiceTests
.WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build()) .WithChapter(new ChapterBuilder("1").WithPages(1).Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("2") .WithVolume(new VolumeBuilder("2")
.WithChapter(new ChapterBuilder("21").WithPages(1).Build()) .WithChapter(new ChapterBuilder("21").WithPages(1).Build())
@ -429,7 +430,7 @@ public class TachiyomiServiceTests
.WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build()) .WithChapter(new ChapterBuilder("1").WithPages(1).Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("2") .WithVolume(new VolumeBuilder("2")
.WithChapter(new ChapterBuilder("3").WithPages(1).Build()) .WithChapter(new ChapterBuilder("3").WithPages(1).Build())
@ -472,7 +473,7 @@ public class TachiyomiServiceTests
.WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build()) .WithChapter(new ChapterBuilder("1").WithPages(1).Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("2") .WithVolume(new VolumeBuilder("2")
.WithChapter(new ChapterBuilder("3").WithPages(1).Build()) .WithChapter(new ChapterBuilder("3").WithPages(1).Build())
@ -570,7 +571,7 @@ public class TachiyomiServiceTests
.WithChapter(new ChapterBuilder("96").WithPages(1).Build()) .WithChapter(new ChapterBuilder("96").WithPages(1).Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("1") .WithVolume(new VolumeBuilder("1")
.WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build()) .WithChapter(new ChapterBuilder("1").WithPages(1).Build())
.Build()) .Build())
.WithVolume(new VolumeBuilder("2") .WithVolume(new VolumeBuilder("2")
.WithChapter(new ChapterBuilder("21").WithPages(1).Build()) .WithChapter(new ChapterBuilder("21").WithPages(1).Build())

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using API.Extensions;
using API.Services.Tasks.Scanner.Parser; using API.Services.Tasks.Scanner.Parser;
namespace API.Comparators; namespace API.Comparators;
@ -6,28 +7,28 @@ namespace API.Comparators;
#nullable enable #nullable enable
/// <summary> /// <summary>
/// Sorts chapters based on their Number. Uses natural ordering of doubles. /// Sorts chapters based on their Number. Uses natural ordering of doubles. Specials always LAST.
/// </summary> /// </summary>
public class ChapterSortComparer : IComparer<double> public class ChapterSortComparerDefaultLast : IComparer<float>
{ {
/// <summary> /// <summary>
/// Normal sort for 2 doubles. 0 always comes last /// Normal sort for 2 doubles. DefaultChapterNumber always comes last
/// </summary> /// </summary>
/// <param name="x"></param> /// <param name="x"></param>
/// <param name="y"></param> /// <param name="y"></param>
/// <returns></returns> /// <returns></returns>
public int Compare(double x, double y) public int Compare(float x, float y)
{ {
if (x == Parser.DefaultChapterNumber && y == Parser.DefaultChapterNumber) return 0; if (x.Is(Parser.DefaultChapterNumber) && y.Is(Parser.DefaultChapterNumber)) return 0;
// if x is 0, it comes second // if x is 0, it comes second
if (x == Parser.DefaultChapterNumber) return 1; if (x.Is(Parser.DefaultChapterNumber)) return 1;
// if y is 0, it comes second // if y is 0, it comes second
if (y == Parser.DefaultChapterNumber) return -1; if (y.Is(Parser.DefaultChapterNumber)) return -1;
return x.CompareTo(y); return x.CompareTo(y);
} }
public static readonly ChapterSortComparer Default = new ChapterSortComparer(); public static readonly ChapterSortComparerDefaultLast Default = new ChapterSortComparerDefaultLast();
} }
/// <summary> /// <summary>
@ -37,33 +38,43 @@ public class ChapterSortComparer : IComparer<double>
/// This is represented by Chapter 0, Chapter 81. /// This is represented by Chapter 0, Chapter 81.
/// </example> /// </example>
/// </summary> /// </summary>
public class ChapterSortComparerZeroFirst : IComparer<double> public class ChapterSortComparerDefaultFirst : IComparer<float>
{ {
public int Compare(double x, double y) public int Compare(float x, float y)
{ {
if (x == Parser.DefaultChapterNumber && y == Parser.DefaultChapterNumber) return 0; if (x.Is(Parser.DefaultChapterNumber) && y.Is(Parser.DefaultChapterNumber)) return 0;
// if x is 0, it comes first // if x is 0, it comes first
if (x == Parser.DefaultChapterNumber) return -1; if (x.Is(Parser.DefaultChapterNumber)) return -1;
// if y is 0, it comes first // if y is 0, it comes first
if (y == Parser.DefaultChapterNumber) return 1; if (y.Is(Parser.DefaultChapterNumber)) return 1;
return x.CompareTo(y); return x.CompareTo(y);
} }
public static readonly ChapterSortComparerZeroFirst Default = new ChapterSortComparerZeroFirst(); public static readonly ChapterSortComparerDefaultFirst Default = new ChapterSortComparerDefaultFirst();
} }
public class SortComparerZeroLast : IComparer<double> /// <summary>
/// Sorts chapters based on their Number. Uses natural ordering of doubles. Specials always LAST.
/// </summary>
public class ChapterSortComparerSpecialsLast : IComparer<float>
{ {
public int Compare(double x, double y) /// <summary>
/// Normal sort for 2 doubles. DefaultSpecialNumber always comes last
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
public int Compare(float x, float y)
{ {
if (x == Parser.DefaultChapterNumber && y == Parser.DefaultChapterNumber) return 0; if (x.Is(Parser.SpecialVolumeNumber) && y.Is(Parser.SpecialVolumeNumber)) return 0;
// if x is 0, it comes last // if x is 0, it comes second
if (x == Parser.DefaultChapterNumber) return 1; if (x.Is(Parser.SpecialVolumeNumber)) return 1;
// if y is 0, it comes last // if y is 0, it comes second
if (y == Parser.DefaultChapterNumber) return -1; if (y.Is(Parser.SpecialVolumeNumber)) return -1;
return x.CompareTo(y); return x.CompareTo(y);
} }
public static readonly SortComparerZeroLast Default = new SortComparerZeroLast();
public static readonly ChapterSortComparerSpecialsLast Default = new ChapterSortComparerSpecialsLast();
} }

View file

@ -140,7 +140,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()}_c{chapterId}", $"{series!.Name} - Chapter {chapter.Number}.zip"); return await DownloadFiles(files, $"download_{User.GetUsername()}_c{chapterId}", $"{series!.Name} - Chapter {chapter.GetNumberTitle()}.zip");
} }
catch (KavitaException ex) catch (KavitaException ex)
{ {

View file

@ -70,7 +70,7 @@ public class OpdsController : BaseApiController
}; };
private readonly FilterV2Dto _filterV2Dto = new FilterV2Dto(); private readonly FilterV2Dto _filterV2Dto = new FilterV2Dto();
private readonly ChapterSortComparer _chapterSortComparer = ChapterSortComparer.Default; private readonly ChapterSortComparerDefaultLast _chapterSortComparerDefaultLast = ChapterSortComparerDefaultLast.Default;
private const int PageSize = 20; private const int PageSize = 20;
public OpdsController(IUnitOfWork unitOfWork, IDownloadService downloadService, public OpdsController(IUnitOfWork unitOfWork, IDownloadService downloadService,
@ -857,8 +857,8 @@ public class OpdsController : BaseApiController
var seriesDetail = await _seriesService.GetSeriesDetail(seriesId, userId); var seriesDetail = await _seriesService.GetSeriesDetail(seriesId, userId);
foreach (var volume in seriesDetail.Volumes) foreach (var volume in seriesDetail.Volumes)
{ {
var chapters = (await _unitOfWork.ChapterRepository.GetChaptersAsync(volume.Id)).OrderBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), var chapters = (await _unitOfWork.ChapterRepository.GetChaptersAsync(volume.Id))
_chapterSortComparer); .OrderBy(x => x.MinNumber, _chapterSortComparerDefaultLast);
foreach (var chapterId in chapters.Select(c => c.Id)) foreach (var chapterId in chapters.Select(c => c.Id))
{ {
@ -907,8 +907,8 @@ public class OpdsController : BaseApiController
var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId); var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId);
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId); var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId);
var chapters = var chapters =
(await _unitOfWork.ChapterRepository.GetChaptersAsync(volumeId)).OrderBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture), (await _unitOfWork.ChapterRepository.GetChaptersAsync(volumeId))
_chapterSortComparer); .OrderBy(x => x.MinNumber, _chapterSortComparerDefaultLast);
var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {_seriesService.FormatChapterName(userId, libraryType)}s ", var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {_seriesService.FormatChapterName(userId, libraryType)}s ",
$"{prefix}{apiKey}/series/{seriesId}/volume/{volumeId}", apiKey, prefix); $"{prefix}{apiKey}/series/{seriesId}/volume/{volumeId}", apiKey, prefix);
SetFeedId(feed, $"series-{series.Id}-volume-{volume.Id}-{_seriesService.FormatChapterName(userId, libraryType)}s"); SetFeedId(feed, $"series-{series.Id}-volume-{volume.Id}-{_seriesService.FormatChapterName(userId, libraryType)}s");
@ -1101,18 +1101,18 @@ public class OpdsController : BaseApiController
var title = $"{series.Name}"; var title = $"{series.Name}";
if (volume!.Chapters.Count == 1) if (volume!.Chapters.Count == 1 && !volume.IsSpecial())
{ {
var volumeLabel = await _localizationService.Translate(userId, "volume-num", string.Empty); var volumeLabel = await _localizationService.Translate(userId, "volume-num", string.Empty);
SeriesService.RenameVolumeName(volume.Chapters.First(), volume, libraryType, volumeLabel); SeriesService.RenameVolumeName(volume, libraryType, volumeLabel);
if (volume.Name != Services.Tasks.Scanner.Parser.Parser.DefaultChapter) if (!volume.IsLooseLeaf())
{ {
title += $" - {volume.Name}"; title += $" - {volume.Name}";
} }
} }
else if (!volume.IsLooseLeaf()) else if (!volume.IsLooseLeaf() && !volume.IsSpecial())
{ {
title = $"{series.Name} - Volume {volume.Name} - {await _seriesService.FormatChapterTitle(userId, chapter, libraryType)}"; title = $"{series.Name} - Volume {volume.Name} - {await _seriesService.FormatChapterTitle(userId, chapter, libraryType)}";
} }
else else
{ {

View file

@ -13,13 +13,17 @@ public class ChapterDto : IHasReadTimeEstimate
{ {
public int Id { get; init; } public int Id { get; init; }
/// <summary> /// <summary>
/// Range of chapters. Chapter 2-4 -> "2-4". Chapter 2 -> "2". /// Range of chapters. Chapter 2-4 -> "2-4". Chapter 2 -> "2". If special, will be special name.
/// </summary> /// </summary>
public string Range { get; init; } = default!; public string Range { get; init; } = default!;
/// <summary> /// <summary>
/// Smallest number of the Range. /// Smallest number of the Range.
/// </summary> /// </summary>
[Obsolete("Use MinNumber and MaxNumber instead")]
public string Number { get; init; } = default!; public string Number { get; init; } = default!;
public float MinNumber { get; init; }
public float MaxNumber { get; init; }
public float SortOrder { get; init; }
/// <summary> /// <summary>
/// Total number of pages in all MangaFiles /// Total number of pages in all MangaFiles
/// </summary> /// </summary>

View file

@ -14,5 +14,5 @@ public class ReadHistoryEvent
public required string SeriesName { get; set; } = default!; public required string SeriesName { get; set; } = default!;
public DateTime ReadDate { get; set; } public DateTime ReadDate { get; set; }
public int ChapterId { get; set; } public int ChapterId { get; set; }
public required string ChapterNumber { get; set; } = default!; public required float ChapterNumber { get; set; } = default!;
} }

View file

@ -0,0 +1,12 @@
namespace API.DTOs;
/// <summary>
/// This is explicitly for Tachiyomi. Number field was removed in v0.8.0, but Tachiyomi needs it for the hacks.
/// </summary>
public class TachiyomiChapterDto : ChapterDto
{
/// <summary>
/// Smallest number of the Range.
/// </summary>
public string Number { get; init; } = default!;
}

View file

@ -3,6 +3,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using API.Entities; using API.Entities;
using API.Entities.Interfaces; using API.Entities.Interfaces;
using API.Extensions;
using API.Services.Tasks.Scanner.Parser; using API.Services.Tasks.Scanner.Parser;
namespace API.DTOs; namespace API.DTOs;
@ -20,7 +21,7 @@ public class VolumeDto : IHasReadTimeEstimate
/// This will map to MinNumber. Number was removed in v0.7.13.8/v0.7.14 /// This will map to MinNumber. Number was removed in v0.7.13.8/v0.7.14
/// </summary> /// </summary>
[Obsolete("Use MinNumber")] [Obsolete("Use MinNumber")]
public float Number { get; set; } public int Number { get; set; }
public int Pages { get; set; } public int Pages { get; set; }
public int PagesRead { get; set; } public int PagesRead { get; set; }
public DateTime LastModifiedUtc { get; set; } public DateTime LastModifiedUtc { get; set; }
@ -50,6 +51,15 @@ public class VolumeDto : IHasReadTimeEstimate
/// <returns></returns> /// <returns></returns>
public bool IsLooseLeaf() public bool IsLooseLeaf()
{ {
return Math.Abs(this.MinNumber - Parser.LooseLeafVolumeNumber) < 0.001f; return MinNumber.Is(Parser.LooseLeafVolumeNumber);
}
/// <summary>
/// Does this volume hold only specials?
/// </summary>
/// <returns></returns>
public bool IsSpecial()
{
return MinNumber.Is(Parser.SpecialVolumeNumber);
} }
} }

View file

@ -156,10 +156,15 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
{ {
if (e.FromQuery || e.Entry.State != EntityState.Added || e.Entry.Entity is not IEntityDate entity) return; if (e.FromQuery || e.Entry.State != EntityState.Added || e.Entry.Entity is not IEntityDate entity) return;
entity.Created = DateTime.Now;
entity.LastModified = DateTime.Now; entity.LastModified = DateTime.Now;
entity.CreatedUtc = DateTime.UtcNow;
entity.LastModifiedUtc = DateTime.UtcNow; entity.LastModifiedUtc = DateTime.UtcNow;
// This allows for mocking
if (entity.Created == DateTime.MinValue)
{
entity.Created = DateTime.Now;
entity.CreatedUtc = DateTime.UtcNow;
}
} }
private static void OnEntityStateChanged(object? sender, EntityStateChangedEventArgs e) private static void OnEntityStateChanged(object? sender, EntityStateChangedEventArgs e)

View file

@ -0,0 +1,140 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using API.Helpers.Builders;
using API.Services.Tasks.Scanner.Parser;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
public class UserProgressCsvRecord
{
public bool IsSpecial { get; set; }
public int AppUserId { get; set; }
public int PagesRead { get; set; }
public string Range { get; set; }
public string Number { get; set; }
public float MinNumber { get; set; }
public int SeriesId { get; set; }
public int VolumeId { get; set; }
}
/// <summary>
/// v0.8.0 migration to move Specials into their own volume and retain user progress.
/// </summary>
public static class MigrateMixedSpecials
{
public static async Task Migrate(DataContext dataContext, IUnitOfWork unitOfWork, ILogger<Program> logger)
{
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateMixedSpecials"))
{
return;
}
logger.LogCritical(
"Running ManualMigrateMixedSpecials migration - Please be patient, this may take some time. This is not an error");
// First, group all the progresses into different series
// Get each series and move the specials from old volume to the new Volume()
// Create a new progress event from existing and store the Id of existing progress event to delete it
// Save per series
var progress = await dataContext.AppUserProgresses
.Join(dataContext.Chapter, p => p.ChapterId, c => c.Id, (p, c) => new UserProgressCsvRecord
{
IsSpecial = c.IsSpecial,
AppUserId = p.AppUserId,
PagesRead = p.PagesRead,
Range = c.Range,
Number = c.Number,
MinNumber = c.MinNumber,
SeriesId = p.SeriesId,
VolumeId = p.VolumeId
})
.Where(d => d.IsSpecial || d.Number == "0")
.Join(dataContext.Volume, d => d.VolumeId, v => v.Id, (d, v) => new
{
ProgressRecord = d,
Volume = v
})
.Where(d => d.Volume.Name == "0")
.ToListAsync();
// First, group all the progresses into different series
logger.LogCritical("Migrating {Count} progress events to new Volume structure - This may take over 10 minutes depending on size of DB. Please wait", progress.Count);
var progressesGroupedBySeries = progress.GroupBy(p => p.ProgressRecord.SeriesId);
foreach (var seriesGroup in progressesGroupedBySeries)
{
// Get each series and move the specials from the old volume to the new Volume
var seriesId = seriesGroup.Key;
var specialsInSeries = seriesGroup
.Where(p => p.ProgressRecord.IsSpecial)
.ToList();
// Get distinct Volumes by Id. For each one, create it then create the progress events
var distinctVolumes = specialsInSeries.DistinctBy(d => d.Volume.Id);
foreach (var distinctVolume in distinctVolumes)
{
// Create a new volume for each series with the appropriate number (-100000)
var chapters = await dataContext.Chapter
.Where(c => c.VolumeId == distinctVolume.Volume.Id && c.IsSpecial).ToListAsync();
var newVolume = new VolumeBuilder(Parser.SpecialVolume)
.WithSeriesId(seriesId)
.WithChapters(chapters)
.Build();
dataContext.Volume.Add(newVolume);
await dataContext.SaveChangesAsync(); // Save changes to generate the newVolumeId
// Migrate the progress event to the new volume
distinctVolume.ProgressRecord.VolumeId = newVolume.Id;
logger.LogInformation("Moving {Count} chapters from Volume Id {OldVolumeId} to New Volume {NewVolumeId}",
chapters.Count, distinctVolume.Volume.Id, newVolume.Id);
// Move the special chapters from the old volume to the new Volume
var specialChapters = await dataContext.Chapter
.Where(c => c.VolumeId == distinctVolume.ProgressRecord.VolumeId && c.IsSpecial)
.ToListAsync();
foreach (var specialChapter in specialChapters)
{
// Update the VolumeId on the existing progress event
specialChapter.VolumeId = newVolume.Id;
}
await dataContext.SaveChangesAsync();
}
}
// Save changes after processing all series
if (dataContext.ChangeTracker.HasChanges())
{
await dataContext.SaveChangesAsync();
}
// Update all Volumes with Name as "0" -> Special
logger.LogCritical("Updating all Volumes with Name 0 to SpecialNumber");
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
{
Name = "ManualMigrateMixedSpecials",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await dataContext.SaveChangesAsync();
logger.LogCritical(
"Running ManualMigrateMixedSpecials migration - Completed. This is not an error");
}
}

View file

@ -0,0 +1,89 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
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.8.0, this migrates the existing Chapter and Volume 0 -> Parser defined, MangaFile.FileName
/// </summary>
public static class MigrateChapterFields
{
public static async Task Migrate(DataContext dataContext, IUnitOfWork unitOfWork, ILogger<Program> logger)
{
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateChapterFields"))
{
return;
}
logger.LogCritical(
"Running MigrateChapterFields migration - Please be patient, this may take some time. This is not an error");
// Update all volumes only have specials in them (rare)
var volumesWithJustSpecials = dataContext.Volume
.Include(v => v.Chapters)
.Where(v => v.Name == "0" && v.Chapters.All(c => c.IsSpecial))
.ToList();
logger.LogCritical(
"Running MigrateChapterFields migration - Updating {Count} volumes that only have specials in them", volumesWithJustSpecials.Count);
foreach (var volume in volumesWithJustSpecials)
{
volume.Name = $"{Parser.SpecialVolumeNumber}";
volume.MinNumber = Parser.SpecialVolumeNumber;
volume.MaxNumber = Parser.SpecialVolumeNumber;
}
// Update all volumes that only have loose leafs in them
var looseLeafVolumes = dataContext.Volume
.Include(v => v.Chapters)
.Where(v => v.Name == "0" && v.Chapters.All(c => !c.IsSpecial))
.ToList();
logger.LogCritical(
"Running MigrateChapterFields migration - Updating {Count} volumes that only have loose leaf chapters in them", looseLeafVolumes.Count);
foreach (var volume in looseLeafVolumes)
{
volume.Name = $"{Parser.DefaultChapterNumber}";
volume.MinNumber = Parser.DefaultChapterNumber;
volume.MaxNumber = Parser.DefaultChapterNumber;
}
// Update all MangaFile
logger.LogCritical(
"Running MigrateChapterFields migration - Updating all MangaFiles");
foreach (var mangaFile in dataContext.MangaFile)
{
mangaFile.FileName = Path.GetFileNameWithoutExtension(mangaFile.FilePath);
}
var looseLeafChapters = await dataContext.Chapter.Where(c => c.Number == "0").ToListAsync();
logger.LogCritical(
"Running MigrateChapterFields migration - Updating {Count} loose leaf chapters", looseLeafChapters.Count);
foreach (var chapter in looseLeafChapters)
{
chapter.Number = Parser.DefaultChapter;
chapter.MinNumber = Parser.DefaultChapterNumber;
chapter.MaxNumber = Parser.DefaultChapterNumber;
}
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
{
Name = "MigrateChapterFields",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await dataContext.SaveChangesAsync();
logger.LogCritical(
"Running MigrateChapterFields migration - Completed. This is not an error");
}
}

View file

@ -0,0 +1,50 @@
using System;
using System.Threading.Tasks;
using API.Entities;
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.8.0, this migrates the existing Chapter Range -> Chapter Min/Max Number
/// </summary>
public static class MigrateChapterNumber
{
public static async Task Migrate(DataContext dataContext, ILogger<Program> logger)
{
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateChapterNumber"))
{
return;
}
logger.LogCritical(
"Running MigrateChapterNumber migration - Please be patient, this may take some time. This is not an error");
// Get all volumes
foreach (var chapter in dataContext.Chapter)
{
if (chapter.IsSpecial)
{
chapter.MinNumber = Parser.DefaultChapterNumber;
chapter.MaxNumber = Parser.DefaultChapterNumber;
continue;
}
chapter.MinNumber = Parser.MinNumberFromRange(chapter.Range);
chapter.MaxNumber = Parser.MaxNumberFromRange(chapter.Range);
}
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
{
Name = "MigrateChapterNumber",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await dataContext.SaveChangesAsync();
logger.LogCritical(
"Running MigrateChapterNumber migration - Completed. This is not an error");
}
}

View file

@ -15,9 +15,8 @@ public static class MigrateLibrariesToHaveAllFileTypes
{ {
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> logger) public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> logger)
{ {
if (await dataContext.Library.AnyAsync(l => l.LibraryFileTypes.Count == 0)) if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateLibrariesToHaveAllFileTypes"))
{ {
logger.LogCritical("Running MigrateLibrariesToHaveAllFileTypes migration - Completed. This is not an error");
return; return;
} }

View file

@ -16,8 +16,6 @@ public static class MigrateManualHistory
{ {
if (await dataContext.ManualMigrationHistory.AnyAsync()) if (await dataContext.ManualMigrationHistory.AnyAsync())
{ {
logger.LogCritical(
"Running MigrateManualHistory migration - Completed. This is not an error");
return; return;
} }

View file

@ -4,6 +4,7 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.DTOs.Filtering.v2; using API.DTOs.Filtering.v2;
using API.Helpers; using API.Helpers;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations; namespace API.Data.ManualMigrations;
@ -21,8 +22,12 @@ public static class MigrateSmartFilterEncoding
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> logger) public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> logger)
{ {
logger.LogCritical("Running MigrateSmartFilterEncoding migration - Please be patient, this may take some time. This is not an error"); if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateSmartFilterEncoding"))
{
return;
}
logger.LogCritical("Running MigrateSmartFilterEncoding migration - Please be patient, this may take some time. This is not an error");
var smartFilters = dataContext.AppUserSmartFilter.ToList(); var smartFilters = dataContext.AppUserSmartFilter.ToList();
foreach (var filter in smartFilters) foreach (var filter in smartFilters)

View file

@ -14,6 +14,10 @@ public static class MigrateUserLibrarySideNavStream
{ {
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> logger) public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> logger)
{ {
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateUserLibrarySideNavStream"))
{
return;
}
var usersWithLibraryStreams = await dataContext.AppUser var usersWithLibraryStreams = await dataContext.AppUser
.Include(u => u.SideNavStreams) .Include(u => u.SideNavStreams)

View file

@ -0,0 +1,41 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
using Kavita.Common.EnvironmentInfo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
public static class MigrateVolumeLookupName
{
public static async Task Migrate(DataContext dataContext, IUnitOfWork unitOfWork, ILogger<Program> logger)
{
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateVolumeLookupName"))
{
return;
}
logger.LogCritical(
"Running MigrateVolumeLookupName migration - Please be patient, this may take some time. This is not an error");
// Update all volumes to have LookupName as after this migration, name isn't used for lookup
var volumes = dataContext.Volume.ToList();
foreach (var volume in volumes)
{
volume.LookupName = volume.Name;
}
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
{
Name = "MigrateVolumeLookupName",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await dataContext.SaveChangesAsync();
logger.LogCritical(
"Running MigrateVolumeLookupName migration - Completed. This is not an error");
}
}

View file

@ -13,8 +13,13 @@ namespace API.Data.ManualMigrations;
/// </summary> /// </summary>
public static class MigrateVolumeNumber public static class MigrateVolumeNumber
{ {
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> logger) public static async Task Migrate(DataContext dataContext, ILogger<Program> logger)
{ {
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateVolumeNumber"))
{
return;
}
if (await dataContext.Volume.AnyAsync(v => v.MaxNumber > 0)) if (await dataContext.Volume.AnyAsync(v => v.MaxNumber > 0))
{ {
logger.LogCritical( logger.LogCritical(

View file

@ -20,6 +20,11 @@ public static class MigrateWantToReadExport
{ {
try try
{ {
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateWantToReadExport"))
{
return;
}
var importFile = Path.Join(directoryService.ConfigDirectory, "want-to-read-migration.csv"); var importFile = Path.Join(directoryService.ConfigDirectory, "want-to-read-migration.csv");
if (File.Exists(importFile)) if (File.Exists(importFile))
{ {

View file

@ -6,6 +6,7 @@ using API.Data.Repositories;
using API.Entities; using API.Entities;
using API.Services; using API.Services;
using CsvHelper; using CsvHelper;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations; namespace API.Data.ManualMigrations;
@ -15,8 +16,14 @@ namespace API.Data.ManualMigrations;
/// </summary> /// </summary>
public static class MigrateWantToReadImport public static class MigrateWantToReadImport
{ {
public static async Task Migrate(IUnitOfWork unitOfWork, IDirectoryService directoryService, ILogger<Program> logger) public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, IDirectoryService directoryService, ILogger<Program> logger)
{ {
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateWantToReadImport"))
{
return;
}
var importFile = Path.Join(directoryService.ConfigDirectory, "want-to-read-migration.csv"); var importFile = Path.Join(directoryService.ConfigDirectory, "want-to-read-migration.csv");
var outputFile = Path.Join(directoryService.ConfigDirectory, "imported-want-to-read-migration.csv"); var outputFile = Path.Join(directoryService.ConfigDirectory, "imported-want-to-read-migration.csv");

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 ChapterNumber : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<float>(
name: "MaxNumber",
table: "Chapter",
type: "REAL",
nullable: false,
defaultValue: 0f);
migrationBuilder.AddColumn<float>(
name: "MinNumber",
table: "Chapter",
type: "REAL",
nullable: false,
defaultValue: 0f);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "MaxNumber",
table: "Chapter");
migrationBuilder.DropColumn(
name: "MinNumber",
table: "Chapter");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class MangaFileNameTemp : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "FileName",
table: "MangaFile",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "FileName",
table: "MangaFile");
}
}
}

File diff suppressed because it is too large Load diff

View file

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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class VolumeLookupName : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "LookupName",
table: "Volume",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "LookupName",
table: "Volume");
}
}
}

View file

@ -679,9 +679,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>("Number") b.Property<string>("Number")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
@ -697,6 +703,9 @@ namespace API.Data.Migrations
b.Property<string>("SeriesGroup") b.Property<string>("SeriesGroup")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<float>("SortOrder")
.HasColumnType("REAL");
b.Property<string>("StoryArc") b.Property<string>("StoryArc")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
@ -973,6 +982,9 @@ namespace API.Data.Migrations
b.Property<string>("Extension") b.Property<string>("Extension")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("FileName")
.HasColumnType("TEXT");
b.Property<string>("FilePath") b.Property<string>("FilePath")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
@ -1839,6 +1851,9 @@ namespace API.Data.Migrations
b.Property<DateTime>("LastModifiedUtc") b.Property<DateTime>("LastModifiedUtc")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("LookupName")
.HasColumnType("TEXT");
b.Property<int>("MaxHoursToRead") b.Property<int>("MaxHoursToRead")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");

View file

@ -167,9 +167,10 @@ public class AppUserProgressRepository : IAppUserProgressRepository
(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.Range) .Where(p => p.chapter.MaxNumber != Parser.SpecialVolumeNumber)
.Select(p => p.chapter.MaxNumber)
.ToListAsync(); .ToListAsync();
return list.Count == 0 ? 0 : list.DefaultIfEmpty().Where(d => d != null).Max(d => (int) Math.Floor(Parser.MaxNumberFromRange(d))); return list.Count == 0 ? 0 : (int) list.DefaultIfEmpty().Max(d => d);
} }
public async Task<float> GetHighestFullyReadVolumeForSeries(int seriesId, int userId) public async Task<float> GetHighestFullyReadVolumeForSeries(int seriesId, int userId)
@ -179,6 +180,7 @@ public class AppUserProgressRepository : IAppUserProgressRepository
(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)
.Where(p => p.chapter.MaxNumber != Parser.SpecialVolumeNumber)
.Select(p => p.chapter.Volume.MaxNumber) .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

@ -78,7 +78,7 @@ public class ChapterRepository : IChapterRepository
.Where(c => c.Id == chapterId) .Where(c => c.Id == chapterId)
.Join(_context.Volume, c => c.VolumeId, v => v.Id, (chapter, volume) => new .Join(_context.Volume, c => c.VolumeId, v => v.Id, (chapter, volume) => new
{ {
ChapterNumber = chapter.Range, ChapterNumber = chapter.MinNumber,
VolumeNumber = volume.Name, VolumeNumber = volume.Name,
VolumeId = volume.Id, VolumeId = volume.Id,
chapter.IsSpecial, chapter.IsSpecial,
@ -102,8 +102,8 @@ public class ChapterRepository : IChapterRepository
}) })
.Select(data => new ChapterInfoDto() .Select(data => new ChapterInfoDto()
{ {
ChapterNumber = data.ChapterNumber, ChapterNumber = data.ChapterNumber + string.Empty, // TODO: Fix this
VolumeNumber = data.VolumeNumber + string.Empty, VolumeNumber = data.VolumeNumber + string.Empty, // TODO: Fix this
VolumeId = data.VolumeId, VolumeId = data.VolumeId,
IsSpecial = data.IsSpecial, IsSpecial = data.IsSpecial,
SeriesId = data.SeriesId, SeriesId = data.SeriesId,
@ -175,6 +175,7 @@ public class ChapterRepository : IChapterRepository
{ {
return await _context.Chapter return await _context.Chapter
.Includes(includes) .Includes(includes)
.OrderBy(c => c.SortOrder)
.FirstOrDefaultAsync(c => c.Id == chapterId); .FirstOrDefaultAsync(c => c.Id == chapterId);
} }
@ -187,6 +188,7 @@ public class ChapterRepository : IChapterRepository
{ {
return await _context.Chapter return await _context.Chapter
.Where(c => c.VolumeId == volumeId) .Where(c => c.VolumeId == volumeId)
.OrderBy(c => c.SortOrder)
.ToListAsync(); .ToListAsync();
} }
@ -267,10 +269,16 @@ public class ChapterRepository : IChapterRepository
return chapter; return chapter;
} }
/// <summary>
/// Includes Volumes
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
public IEnumerable<Chapter> GetChaptersForSeries(int seriesId) public IEnumerable<Chapter> GetChaptersForSeries(int seriesId)
{ {
return _context.Chapter return _context.Chapter
.Where(c => c.Volume.SeriesId == seriesId) .Where(c => c.Volume.SeriesId == seriesId)
.OrderBy(c => c.SortOrder)
.Include(c => c.Volume) .Include(c => c.Volume)
.AsEnumerable(); .AsEnumerable();
} }

View file

@ -1891,8 +1891,8 @@ public class SeriesRepository : ISeriesRepository
VolumeId = c.VolumeId, VolumeId = c.VolumeId,
ChapterId = c.Id, ChapterId = c.Id,
Format = c.Volume.Series.Format, Format = c.Volume.Series.Format,
ChapterNumber = c.Number, ChapterNumber = c.MinNumber + string.Empty, // TODO: Refactor this
ChapterRange = c.Range, ChapterRange = c.Range, // TODO: Refactor this
IsSpecial = c.IsSpecial, IsSpecial = c.IsSpecial,
VolumeNumber = c.Volume.MinNumber, VolumeNumber = c.Volume.MinNumber,
ChapterTitle = c.Title, ChapterTitle = c.Title,

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -6,6 +7,7 @@ using API.DTOs;
using API.Entities; using API.Entities;
using API.Entities.Enums; using API.Entities.Enums;
using API.Extensions; using API.Extensions;
using API.Extensions.QueryExtensions;
using API.Services; using API.Services;
using AutoMapper; using AutoMapper;
using AutoMapper.QueryableExtensions; using AutoMapper.QueryableExtensions;
@ -14,6 +16,15 @@ using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories; namespace API.Data.Repositories;
[Flags]
public enum VolumeIncludes
{
None = 1,
Chapters = 2,
People = 4,
Tags = 8,
}
public interface IVolumeRepository public interface IVolumeRepository
{ {
void Add(Volume volume); void Add(Volume volume);
@ -22,7 +33,7 @@ public interface IVolumeRepository
Task<IList<MangaFile>> GetFilesForVolume(int volumeId); Task<IList<MangaFile>> GetFilesForVolume(int volumeId);
Task<string?> GetVolumeCoverImageAsync(int volumeId); Task<string?> GetVolumeCoverImageAsync(int volumeId);
Task<IList<int>> GetChapterIdsByVolumeIds(IReadOnlyList<int> volumeIds); Task<IList<int>> GetChapterIdsByVolumeIds(IReadOnlyList<int> volumeIds);
Task<IEnumerable<VolumeDto>> GetVolumesDtoAsync(int seriesId, int userId); Task<IList<VolumeDto>> GetVolumesDtoAsync(int seriesId, int userId, VolumeIncludes includes = VolumeIncludes.Chapters);
Task<Volume?> GetVolumeAsync(int volumeId); Task<Volume?> GetVolumeAsync(int volumeId);
Task<VolumeDto?> GetVolumeDtoAsync(int volumeId, int userId); Task<VolumeDto?> GetVolumeDtoAsync(int volumeId, int userId);
Task<IEnumerable<Volume>> GetVolumesForSeriesAsync(IList<int> seriesIds, bool includeChapters = false); Task<IEnumerable<Volume>> GetVolumesForSeriesAsync(IList<int> seriesIds, bool includeChapters = false);
@ -129,6 +140,7 @@ public class VolumeRepository : IVolumeRepository
.Include(vol => vol.Chapters) .Include(vol => vol.Chapters)
.ThenInclude(c => c.Files) .ThenInclude(c => c.Files)
.AsSplitQuery() .AsSplitQuery()
.OrderBy(v => v.MinNumber)
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider) .ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
.SingleOrDefaultAsync(vol => vol.Id == volumeId); .SingleOrDefaultAsync(vol => vol.Id == volumeId);
@ -177,22 +189,22 @@ public class VolumeRepository : IVolumeRepository
/// <param name="seriesId"></param> /// <param name="seriesId"></param>
/// <param name="userId"></param> /// <param name="userId"></param>
/// <returns></returns> /// <returns></returns>
public async Task<IEnumerable<VolumeDto>> GetVolumesDtoAsync(int seriesId, int userId) public async Task<IList<VolumeDto>> GetVolumesDtoAsync(int seriesId, int userId, VolumeIncludes includes = VolumeIncludes.Chapters)
{ {
var volumes = await _context.Volume var volumes = await _context.Volume
.Where(vol => vol.SeriesId == seriesId) .Where(vol => vol.SeriesId == seriesId)
.Include(vol => vol.Chapters) .Includes(includes)
.ThenInclude(c => c.People)
.Include(vol => vol.Chapters)
.ThenInclude(c => c.Tags)
.OrderBy(volume => volume.MinNumber) .OrderBy(volume => volume.MinNumber)
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider) .ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
.AsNoTracking()
.AsSplitQuery() .AsSplitQuery()
.ToListAsync(); .ToListAsync();
await AddVolumeModifiers(userId, volumes); await AddVolumeModifiers(userId, volumes);
SortSpecialChapters(volumes);
foreach (var volume in volumes)
{
volume.Chapters = volume.Chapters.OrderBy(c => c.SortOrder).ToList();
}
return volumes; return volumes;
} }
@ -213,15 +225,6 @@ public class VolumeRepository : IVolumeRepository
} }
private static void SortSpecialChapters(IEnumerable<VolumeDto> volumes)
{
foreach (var v in volumes.WhereLooseLeaf())
{
v.Chapters = v.Chapters.OrderByNatural(x => x.Range).ToList();
}
}
private async Task AddVolumeModifiers(int userId, IReadOnlyCollection<VolumeDto> volumes) private async Task AddVolumeModifiers(int userId, IReadOnlyCollection<VolumeDto> volumes)
{ {
var volIds = volumes.Select(s => s.Id); var volIds = volumes.Select(s => s.Id);

View file

@ -1,7 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using API.Entities.Enums; using API.Entities.Enums;
using API.Entities.Interfaces; using API.Entities.Interfaces;
using API.Extensions;
using API.Services.Tasks.Scanner.Parser; using API.Services.Tasks.Scanner.Parser;
namespace API.Entities; namespace API.Entities;
@ -10,14 +12,27 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate
{ {
public int Id { get; set; } public int Id { get; set; }
/// <summary> /// <summary>
/// Range of numbers. Chapter 2-4 -> "2-4". Chapter 2 -> "2". /// Range of numbers. Chapter 2-4 -> "2-4". Chapter 2 -> "2". If the chapter is a special, will return the Special Name
/// </summary> /// </summary>
public required string Range { get; set; } public required string Range { get; set; }
/// <summary> /// <summary>
/// Smallest number of the Range. Can be a partial like Chapter 4.5 /// Smallest number of the Range. Can be a partial like Chapter 4.5
/// </summary> /// </summary>
[Obsolete("Use MinNumber and MaxNumber instead")]
public required string Number { get; set; } public required string Number { get; set; }
/// <summary> /// <summary>
/// Minimum Chapter Number.
/// </summary>
public float MinNumber { get; set; }
/// <summary>
/// Maximum Chapter Number
/// </summary>
public float MaxNumber { get; set; }
/// <summary>
/// The sorting order of the Chapter. Inherits from MinNumber, but can be overridden.
/// </summary>
public float SortOrder { get; set; }
/// <summary>
/// The files that represent this Chapter /// The files that represent this Chapter
/// </summary> /// </summary>
public ICollection<MangaFile> Files { get; set; } = null!; public ICollection<MangaFile> Files { get; set; } = null!;
@ -44,6 +59,7 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate
/// Used for books/specials to display custom title. For non-specials/books, will be set to <see cref="Range"/> /// Used for books/specials to display custom title. For non-specials/books, will be set to <see cref="Range"/>
/// </summary> /// </summary>
public string? Title { get; set; } public string? Title { get; set; }
/// <summary> /// <summary>
/// Age Rating for the issue/chapter /// Age Rating for the issue/chapter
/// </summary> /// </summary>
@ -130,10 +146,42 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate
if (IsSpecial) if (IsSpecial)
{ {
Number = Parser.DefaultChapter; Number = Parser.DefaultChapter;
MinNumber = Parser.DefaultChapterNumber;
MaxNumber = Parser.DefaultChapterNumber;
} }
Title = (IsSpecial && info.Format == MangaFormat.Epub) Title = (IsSpecial && info.Format == MangaFormat.Epub)
? info.Title ? info.Title
: Range; : Path.GetFileNameWithoutExtension(Range);
} }
/// <summary>
/// Returns the Chapter Number. If the chapter is a range, returns that, formatted.
/// </summary>
/// <returns></returns>
public string GetNumberTitle()
{
if (MinNumber.Is(MaxNumber))
{
if (MinNumber.Is(Parser.DefaultChapterNumber) && IsSpecial)
{
return Title;
}
else
{
return $"{MinNumber}";
}
}
return $"{MinNumber}-{MaxNumber}";
}
/// <summary>
/// Is the Chapter representing a single Volume (volume 1.cbz). If so, Min/Max will be Default and will not be special
/// </summary>
/// <returns></returns>
public bool IsSingleVolumeChapter()
{
return MinNumber.Is(Parser.DefaultChapterNumber) && !IsSpecial;
}
} }

View file

@ -13,6 +13,10 @@ public class MangaFile : IEntityDate
{ {
public int Id { get; set; } public int Id { get; set; }
/// <summary> /// <summary>
/// The filename without extension
/// </summary>
public string FileName { get; set; }
/// <summary>
/// Absolute path to the archive file /// Absolute path to the archive file
/// </summary> /// </summary>
public required string FilePath { get; set; } public required string FilePath { get; set; }

View file

@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using API.Entities.Interfaces; using API.Entities.Interfaces;
using API.Extensions;
using API.Services.Tasks.Scanner.Parser;
namespace API.Entities; namespace API.Entities;
@ -13,6 +15,10 @@ public class Volume : IEntityDate, IHasReadTimeEstimate
/// <remarks>For Books with Series_index, this will map to the Series Index.</remarks> /// <remarks>For Books with Series_index, this will map to the Series Index.</remarks>
public required string Name { get; set; } public required string Name { get; set; }
/// <summary> /// <summary>
/// This is just the original Parsed volume number for lookups
/// </summary>
public string LookupName { get; set; }
/// <summary>
/// The minimum number in the Name field in Int form /// The minimum number in the Name field in Int form
/// </summary> /// </summary>
/// <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> /// <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>
@ -55,4 +61,17 @@ public class Volume : IEntityDate, IHasReadTimeEstimate
public Series Series { get; set; } = null!; public Series Series { get; set; } = null!;
public int SeriesId { get; set; } public int SeriesId { get; set; }
/// <summary>
/// Returns the Chapter Number. If the chapter is a range, returns that, formatted.
/// </summary>
/// <returns></returns>
public string GetNumberTitle()
{
if (MinNumber.Is(MaxNumber))
{
return $"{MinNumber}";
}
return $"{MinNumber}-{MaxNumber}";
}
} }

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using API.Entities; using API.Entities;
using API.Helpers; using API.Helpers;
@ -30,7 +31,7 @@ public static class ChapterListExtensions
{ {
var specialTreatment = info.IsSpecialInfo(); var specialTreatment = info.IsSpecialInfo();
return specialTreatment return specialTreatment
? chapters.FirstOrDefault(c => c.Range == info.Filename || (c.Files.Select(f => f.FilePath).Contains(info.FullFilePath))) ? chapters.FirstOrDefault(c => c.Range == Path.GetFileNameWithoutExtension(info.Filename) || (c.Files.Select(f => f.FilePath).Contains(info.FullFilePath)))
: chapters.FirstOrDefault(c => c.Range == info.Chapters); : chapters.FirstOrDefault(c => c.Range == info.Chapters);
} }

View file

@ -0,0 +1,26 @@
using System;
namespace API.Extensions;
public static class FloatExtensions
{
private const float Tolerance = 0.001f;
/// <summary>
/// Used to compare 2 floats together
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns></returns>
public static bool Is(this float a, float? b)
{
if (!b.HasValue) return false;
return Math.Abs((float) (a - b)) < Tolerance;
}
public static bool IsNot(this float a, float? b)
{
if (!b.HasValue) return false;
return Math.Abs((float) (a - b)) > Tolerance;
}
}

View file

@ -39,6 +39,31 @@ public static class IncludesExtensions
return queryable.AsSplitQuery(); return queryable.AsSplitQuery();
} }
public static IQueryable<Volume> Includes(this IQueryable<Volume> queryable,
VolumeIncludes includes)
{
if (includes.HasFlag(VolumeIncludes.Chapters))
{
queryable = queryable.Include(vol => vol.Chapters);
}
if (includes.HasFlag(VolumeIncludes.People))
{
queryable = queryable
.Include(vol => vol.Chapters)
.ThenInclude(c => c.People);
}
if (includes.HasFlag(VolumeIncludes.Tags))
{
queryable = queryable
.Include(vol => vol.Chapters)
.ThenInclude(c => c.Tags);
}
return queryable.AsSplitQuery();
}
public static IQueryable<Series> Includes(this IQueryable<Series> query, public static IQueryable<Series> Includes(this IQueryable<Series> query,
SeriesIncludes includeFlags) SeriesIncludes includeFlags)
{ {

View file

@ -19,13 +19,13 @@ public static class SeriesExtensions
public static string? GetCoverImage(this Series series) public static string? GetCoverImage(this Series series)
{ {
var volumes = (series.Volumes ?? []) var volumes = (series.Volumes ?? [])
.OrderBy(v => v.MinNumber, ChapterSortComparer.Default) .OrderBy(v => v.MinNumber, ChapterSortComparerDefaultLast.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;
var chapters = firstVolume.Chapters var chapters = firstVolume.Chapters
.OrderBy(c => c.Number.AsDouble(), ChapterSortComparerZeroFirst.Default) .OrderBy(c => c.SortOrder, ChapterSortComparerDefaultLast.Default)
.ToList(); .ToList();
if (chapters.Count > 1 && chapters.Exists(c => c.IsSpecial)) if (chapters.Count > 1 && chapters.Exists(c => c.IsSpecial))
@ -41,25 +41,36 @@ public static class SeriesExtensions
// 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].MinNumber}" != Parser.LooseLeafVolume) if (volumes.Count >= 1 && volumes[0].MinNumber.IsNot(Parser.LooseLeafVolumeNumber))
{ {
var looseLeafChapters = volumes.Where(v => $"{v.MinNumber}" == Parser.LooseLeafVolume) var looseLeafChapters = volumes.Where(v => v.MinNumber.Is(Parser.LooseLeafVolumeNumber))
.SelectMany(c => c.Chapters.Where(c => !c.IsSpecial)) .SelectMany(c => c.Chapters.Where(c2 => !c2.IsSpecial))
.OrderBy(c => c.Number.AsDouble(), ChapterSortComparerZeroFirst.Default) .OrderBy(c => c.MinNumber, ChapterSortComparerDefaultFirst.Default)
.ToList(); .ToList();
if (looseLeafChapters.Count > 0 && (1.0f * volumes[0].MinNumber) > looseLeafChapters[0].Number.AsFloat())
if (looseLeafChapters.Count > 0 && volumes[0].MinNumber > looseLeafChapters[0].MinNumber)
{ {
return looseLeafChapters[0].CoverImage; return looseLeafChapters[0].CoverImage;
} }
return firstVolume.CoverImage; return firstVolume.CoverImage;
} }
var firstLooseLeafChapter = volumes var chpts = volumes
.Where(v => $"{v.MinNumber}" == Parser.LooseLeafVolume) .First(v => v.MinNumber.Is(Parser.LooseLeafVolumeNumber))
.SelectMany(v => v.Chapters) .Chapters
.OrderBy(c => c.Number.AsDouble(), ChapterSortComparerZeroFirst.Default) //.Where(v => v.MinNumber.Is(Parser.LooseLeafVolumeNumber))
.FirstOrDefault(c => !c.IsSpecial); //.SelectMany(v => v.Chapters)
return firstLooseLeafChapter?.CoverImage ?? firstVolume.CoverImage; .Where(c => !c.IsSpecial)
.OrderBy(c => c.MinNumber, ChapterSortComparerDefaultLast.Default)
.ToList();
var exactlyChapter1 = chpts.FirstOrDefault(c => c.MinNumber.Is(1f));
if (exactlyChapter1 != null)
{
return exactlyChapter1.CoverImage;
}
return chpts.FirstOrDefault()?.CoverImage ?? firstVolume.CoverImage;
} }
} }

View file

@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using API.Comparators;
using API.DTOs; using API.DTOs;
using API.Entities; using API.Entities;
using API.Entities.Enums; using API.Entities.Enums;
@ -24,7 +25,7 @@ public static class VolumeListExtensions
{ {
if (volumes == null) throw new ArgumentException("Volumes cannot be null"); if (volumes == null) throw new ArgumentException("Volumes cannot be null");
if (seriesFormat == MangaFormat.Epub || seriesFormat == MangaFormat.Pdf) if (seriesFormat is MangaFormat.Epub or MangaFormat.Pdf)
{ {
return volumes.MinBy(x => x.MinNumber); return volumes.MinBy(x => x.MinNumber);
} }
@ -45,7 +46,7 @@ public static class VolumeListExtensions
/// <returns></returns> /// <returns></returns>
public static bool HasAnyNonLooseLeafVolumes(this IEnumerable<Volume> volumes) public static bool HasAnyNonLooseLeafVolumes(this IEnumerable<Volume> volumes)
{ {
return volumes.Any(x => Math.Abs(x.MinNumber - Parser.DefaultChapterNumber) > 0.001f); return volumes.Any(v => v.MinNumber.IsNot(Parser.DefaultChapterNumber));
} }
/// <summary> /// <summary>
@ -55,7 +56,8 @@ public static class VolumeListExtensions
/// <returns></returns> /// <returns></returns>
public static Volume? FirstNonLooseLeafOrDefault(this IEnumerable<Volume> volumes) public static Volume? FirstNonLooseLeafOrDefault(this IEnumerable<Volume> volumes)
{ {
return volumes.OrderBy(x => x.MinNumber).FirstOrDefault(v => Math.Abs(v.MinNumber - Parser.DefaultChapterNumber) >= 0.001f); return volumes.OrderBy(x => x.MinNumber, ChapterSortComparerDefaultLast.Default)
.FirstOrDefault(v => v.MinNumber.IsNot(Parser.DefaultChapterNumber));
} }
/// <summary> /// <summary>
@ -65,16 +67,16 @@ public static class VolumeListExtensions
/// <returns></returns> /// <returns></returns>
public static Volume? GetLooseLeafVolumeOrDefault(this IEnumerable<Volume> volumes) public static Volume? GetLooseLeafVolumeOrDefault(this IEnumerable<Volume> volumes)
{ {
return volumes.FirstOrDefault(v => Math.Abs(v.MinNumber - Parser.DefaultChapterNumber) < 0.001f); return volumes.FirstOrDefault(v => v.MinNumber.Is(Parser.DefaultChapterNumber));
} }
public static IEnumerable<VolumeDto> WhereNotLooseLeaf(this IEnumerable<VolumeDto> volumes) public static IEnumerable<VolumeDto> WhereNotLooseLeaf(this IEnumerable<VolumeDto> volumes)
{ {
return volumes.Where(v => Math.Abs(v.MinNumber - Parser.DefaultChapterNumber) >= 0.001f); return volumes.Where(v => v.MinNumber.Is(Parser.DefaultChapterNumber));
} }
public static IEnumerable<VolumeDto> WhereLooseLeaf(this IEnumerable<VolumeDto> volumes) public static IEnumerable<VolumeDto> WhereLooseLeaf(this IEnumerable<VolumeDto> volumes)
{ {
return volumes.Where(v => Math.Abs(v.MinNumber - Parser.DefaultChapterNumber) < 0.001f); return volumes.Where(v => v.MinNumber.Is(Parser.DefaultChapterNumber));
} }
} }

View file

@ -47,7 +47,7 @@ public class AutoMapperProfiles : Profile
.ForMember(dest => dest.Series, opt => opt.MapFrom(src => src.Series)); .ForMember(dest => dest.Series, opt => opt.MapFrom(src => src.Series));
CreateMap<LibraryDto, Library>(); CreateMap<LibraryDto, Library>();
CreateMap<Volume, VolumeDto>() CreateMap<Volume, VolumeDto>()
.ForMember(dest => dest.Number, opt => opt.MapFrom(src => src.MinNumber)); .ForMember(dest => dest.Number, opt => opt.MapFrom(src => (int) src.MinNumber));
CreateMap<MangaFile, MangaFileDto>(); CreateMap<MangaFile, MangaFileDto>();
CreateMap<Chapter, ChapterDto>(); CreateMap<Chapter, ChapterDto>();
CreateMap<Series, SeriesDto>(); CreateMap<Series, SeriesDto>();
@ -200,6 +200,8 @@ public class AutoMapperProfiles : Profile
CreateMap<ReadingList, ReadingListDto>(); CreateMap<ReadingList, ReadingListDto>();
CreateMap<ReadingListItem, ReadingListItemDto>(); CreateMap<ReadingListItem, ReadingListItemDto>();
CreateMap<ScrobbleError, ScrobbleErrorDto>(); CreateMap<ScrobbleError, ScrobbleErrorDto>();
CreateMap<ChapterDto, TachiyomiChapterDto>();
CreateMap<Chapter, TachiyomiChapterDto>();
CreateMap<Series, SearchResultDto>() CreateMap<Series, SearchResultDto>()
.ForMember(dest => dest.SeriesId, .ForMember(dest => dest.SeriesId,

View file

@ -20,8 +20,12 @@ public class ChapterBuilder : IEntityBuilder<Chapter>
Range = string.IsNullOrEmpty(range) ? number : range, Range = string.IsNullOrEmpty(range) ? number : range,
Title = string.IsNullOrEmpty(range) ? number : range, Title = string.IsNullOrEmpty(range) ? number : range,
Number = Parser.MinNumberFromRange(number).ToString(CultureInfo.InvariantCulture), Number = Parser.MinNumberFromRange(number).ToString(CultureInfo.InvariantCulture),
MinNumber = Parser.MinNumberFromRange(number),
MaxNumber = Parser.MaxNumberFromRange(number),
SortOrder = Parser.MinNumberFromRange(number),
Files = new List<MangaFile>(), Files = new List<MangaFile>(),
Pages = 1 Pages = 1,
CreatedUtc = DateTime.UtcNow
}; };
} }
@ -30,11 +34,15 @@ public class ChapterBuilder : IEntityBuilder<Chapter>
var specialTreatment = info.IsSpecialInfo(); var specialTreatment = info.IsSpecialInfo();
var specialTitle = specialTreatment ? info.Filename : info.Chapters; var specialTitle = specialTreatment ? info.Filename : info.Chapters;
var builder = new ChapterBuilder(Parser.DefaultChapter); var builder = new ChapterBuilder(Parser.DefaultChapter);
// TODO: Come back here and remove this side effect
return builder.WithNumber(specialTreatment ? Parser.DefaultChapter : Parser.MinNumberFromRange(info.Chapters) + string.Empty) return builder.WithNumber(specialTreatment ? Parser.DefaultChapter : Parser.MinNumberFromRange(info.Chapters) + string.Empty)
.WithRange(specialTreatment ? info.Filename : info.Chapters) .WithRange(specialTreatment ? info.Filename : info.Chapters)
.WithTitle((specialTreatment && info.Format == MangaFormat.Epub) .WithTitle((specialTreatment && info.Format == MangaFormat.Epub)
? info.Title ? info.Title
: specialTitle) : specialTitle)
// NEW
//.WithTitle(string.IsNullOrEmpty(info.Filename) ? specialTitle : info.Filename)
.WithTitle(info.Filename)
.WithIsSpecial(specialTreatment); .WithIsSpecial(specialTreatment);
} }
@ -44,9 +52,18 @@ public class ChapterBuilder : IEntityBuilder<Chapter>
return this; return this;
} }
public ChapterBuilder WithNumber(string number) public ChapterBuilder WithNumber(string number)
{ {
_chapter.Number = number; _chapter.Number = number;
_chapter.MinNumber = Parser.MinNumberFromRange(number);
_chapter.MaxNumber = Parser.MaxNumberFromRange(number);
return this;
}
public ChapterBuilder WithSortOrder(float order)
{
_chapter.SortOrder = order;
return this; return this;
} }
@ -65,6 +82,8 @@ public class ChapterBuilder : IEntityBuilder<Chapter>
private ChapterBuilder WithRange(string range) private ChapterBuilder WithRange(string range)
{ {
_chapter.Range = range; _chapter.Range = range;
// TODO: HACK: Overriding range
_chapter.Range = _chapter.GetNumberTitle();
return this; return this;
} }

View file

@ -19,6 +19,7 @@ public class MangaFileBuilder : IEntityBuilder<MangaFile>
Pages = pages, Pages = pages,
LastModified = File.GetLastWriteTime(filePath), LastModified = File.GetLastWriteTime(filePath),
LastModifiedUtc = File.GetLastWriteTimeUtc(filePath), LastModifiedUtc = File.GetLastWriteTimeUtc(filePath),
FileName = Path.GetFileNameWithoutExtension(filePath)
}; };
} }

View file

@ -26,7 +26,9 @@ public class SeriesBuilder : IEntityBuilder<Series>
SortName = name, SortName = name,
NormalizedName = name.ToNormalized(), NormalizedName = name.ToNormalized(),
NormalizedLocalizedName = name.ToNormalized(), NormalizedLocalizedName = name.ToNormalized(),
Metadata = new SeriesMetadataBuilder().Build(), Metadata = new SeriesMetadataBuilder()
.WithPublicationStatus(PublicationStatus.OnGoing)
.Build(),
Volumes = new List<Volume>(), Volumes = new List<Volume>(),
ExternalSeriesMetadata = new ExternalSeriesMetadata() ExternalSeriesMetadata = new ExternalSeriesMetadata()
}; };
@ -90,4 +92,10 @@ public class SeriesBuilder : IEntityBuilder<Series>
_series.LibraryId = id; _series.LibraryId = id;
return this; return this;
} }
public SeriesBuilder WithPublicationStatus(PublicationStatus status)
{
_series.Metadata.PublicationStatus = status;
return this;
}
} }

View file

@ -15,6 +15,7 @@ public class VolumeBuilder : IEntityBuilder<Volume>
_volume = new Volume() _volume = new Volume()
{ {
Name = volumeNumber, Name = volumeNumber,
LookupName = volumeNumber,
MinNumber = Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(volumeNumber), MinNumber = Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(volumeNumber),
MaxNumber = Services.Tasks.Scanner.Parser.Parser.MaxNumberFromRange(volumeNumber), MaxNumber = Services.Tasks.Scanner.Parser.Parser.MaxNumberFromRange(volumeNumber),
Chapters = new List<Chapter>() Chapters = new List<Chapter>()
@ -49,7 +50,7 @@ public class VolumeBuilder : IEntityBuilder<Volume>
return this; return this;
} }
public VolumeBuilder WithChapters(List<Chapter> chapters) public VolumeBuilder WithChapters(IList<Chapter> chapters)
{ {
_volume.Chapters = chapters; _volume.Chapters = chapters;
return this; return this;

View file

@ -657,7 +657,7 @@ public class DirectoryService : IDirectoryService
/// <returns></returns> /// <returns></returns>
public IList<string> ScanFiles(string folderPath, string fileTypes, GlobMatcher? matcher = null) public IList<string> ScanFiles(string folderPath, string fileTypes, GlobMatcher? matcher = null)
{ {
_logger.LogDebug("[ScanFiles] called on {Path}", folderPath); _logger.LogTrace("[ScanFiles] called on {Path}", folderPath);
var files = new List<string>(); var files = new List<string>();
if (!Exists(folderPath)) return files; if (!Exists(folderPath)) return files;

View file

@ -197,7 +197,7 @@ public class MediaConversionService : IMediaConversionService
foreach (var volume in nonCustomOrConvertedVolumeCovers) foreach (var volume in nonCustomOrConvertedVolumeCovers)
{ {
if (string.IsNullOrEmpty(volume.CoverImage)) continue; if (string.IsNullOrEmpty(volume.CoverImage)) continue;
volume.CoverImage = volume.Chapters.MinBy(x => x.Number.AsDouble(), ChapterSortComparerZeroFirst.Default)?.CoverImage; volume.CoverImage = volume.Chapters.MinBy(x => x.MinNumber, ChapterSortComparerDefaultFirst.Default)?.CoverImage;
_unitOfWork.VolumeRepository.Update(volume); _unitOfWork.VolumeRepository.Update(volume);
await _unitOfWork.CommitAsync(); await _unitOfWork.CommitAsync();
} }

View file

@ -108,7 +108,7 @@ public class MetadataService : IMetadataService
volume.Chapters ??= new List<Chapter>(); volume.Chapters ??= new List<Chapter>();
var firstChapter = volume.Chapters.MinBy(x => x.Number.AsDouble(), ChapterSortComparerZeroFirst.Default); var firstChapter = volume.Chapters.MinBy(x => x.MinNumber, ChapterSortComparerDefaultFirst.Default);
if (firstChapter == null) return Task.FromResult(false); if (firstChapter == null) return Task.FromResult(false);
volume.CoverImage = firstChapter.CoverImage; volume.CoverImage = firstChapter.CoverImage;

View file

@ -11,7 +11,9 @@ using API.DTOs.Scrobbling;
using API.Entities; using API.Entities;
using API.Entities.Enums; using API.Entities.Enums;
using API.Entities.Scrobble; using API.Entities.Scrobble;
using API.Extensions;
using API.Helpers; using API.Helpers;
using API.Services.Tasks.Scanner.Parser;
using API.SignalR; using API.SignalR;
using Flurl.Http; using Flurl.Http;
using Hangfire; using Hangfire;
@ -330,6 +332,15 @@ public class ScrobblingService : IScrobblingService
await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(seriesId, userId), await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(seriesId, userId),
Format = LibraryTypeHelper.GetFormat(series.Library.Type), Format = LibraryTypeHelper.GetFormat(series.Library.Type),
}; };
// NOTE: Not sure how to handle scrobbling specials or handling sending loose leaf volumes
if (evt.VolumeNumber is Parser.SpecialVolumeNumber)
{
evt.VolumeNumber = 0;
}
if (evt.VolumeNumber is Parser.DefaultChapterNumber)
{
evt.VolumeNumber = 0;
}
_unitOfWork.ScrobbleRepository.Attach(evt); _unitOfWork.ScrobbleRepository.Attach(evt);
await _unitOfWork.CommitAsync(); await _unitOfWork.CommitAsync();
_logger.LogDebug("Added Scrobbling Read update on {SeriesName} with Userid {UserId} ", series.Name, userId); _logger.LogDebug("Added Scrobbling Read update on {SeriesName} with Userid {UserId} ", series.Name, userId);
@ -798,7 +809,7 @@ public class ScrobblingService : IScrobblingService
SeriesId = evt.SeriesId SeriesId = evt.SeriesId
}); });
evt.IsErrored = true; evt.IsErrored = true;
evt.ErrorDetails = "Series cannot be matched for Scrobbling"; evt.ErrorDetails = UnknownSeriesErrorMessage;
evt.ProcessDateUtc = DateTime.UtcNow; evt.ProcessDateUtc = DateTime.UtcNow;
_unitOfWork.ScrobbleRepository.Update(evt); _unitOfWork.ScrobbleRepository.Update(evt);
await _unitOfWork.CommitAsync(); await _unitOfWork.CommitAsync();

View file

@ -51,8 +51,9 @@ public class ReaderService : IReaderService
private readonly IImageService _imageService; private readonly IImageService _imageService;
private readonly IDirectoryService _directoryService; private readonly IDirectoryService _directoryService;
private readonly IScrobblingService _scrobblingService; private readonly IScrobblingService _scrobblingService;
private readonly ChapterSortComparer _chapterSortComparer = ChapterSortComparer.Default; private readonly ChapterSortComparerDefaultLast _chapterSortComparerDefaultLast = ChapterSortComparerDefaultLast.Default;
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = ChapterSortComparerZeroFirst.Default; private readonly ChapterSortComparerDefaultFirst _chapterSortComparerForInChapterSorting = ChapterSortComparerDefaultFirst.Default;
private readonly ChapterSortComparerSpecialsLast _chapterSortComparerSpecialsLast = ChapterSortComparerSpecialsLast.Default;
private const float MinWordsPerHour = 10260F; private const float MinWordsPerHour = 10260F;
private const float MaxWordsPerHour = 30000F; private const float MaxWordsPerHour = 30000F;
@ -346,11 +347,23 @@ public class ReaderService : IReaderService
return page; return page;
} }
private int GetNextSpecialChapter(VolumeDto volume, ChapterDto currentChapter)
{
if (volume.IsSpecial())
{
// Handle specials by sorting on their Filename aka Range
return GetNextChapterId(volume.Chapters.OrderBy(x => x.SortOrder), currentChapter.SortOrder, dto => dto.SortOrder);
}
return -1;
}
/// <summary> /// <summary>
/// Tries to find the next logical Chapter /// Tries to find the next logical Chapter
/// </summary> /// </summary>
/// <example> /// <example>
/// V1 → V2 → V3 chapter 0 → V3 chapter 10 → V0 chapter 1 -> V0 chapter 2 -> SP 01 → SP 02 /// V1 → V2 → V3 chapter 0 → V3 chapter 10 → V0 chapter 1 -> V0 chapter 2 -> (Annual 1 -> Annual 2) -> (SP 01 → SP 02)
/// </example> /// </example>
/// <param name="seriesId"></param> /// <param name="seriesId"></param>
/// <param name="volumeId"></param> /// <param name="volumeId"></param>
@ -359,112 +372,88 @@ public class ReaderService : IReaderService
/// <returns>-1 if nothing can be found</returns> /// <returns>-1 if nothing can be found</returns>
public async Task<int> GetNextChapterIdAsync(int seriesId, int volumeId, int currentChapterId, int userId) public async Task<int> GetNextChapterIdAsync(int seriesId, int volumeId, int currentChapterId, int userId)
{ {
var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId)) var volumes = await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId);
.ToList();
var currentVolume = volumes.Single(v => v.Id == volumeId); var currentVolume = volumes.FirstOrDefault(v => v.Id == volumeId);
var currentChapter = currentVolume.Chapters.Single(c => c.Id == currentChapterId); if (currentVolume == null)
{
// Handle the case where the current volume is not found
return -1;
}
var currentChapter = currentVolume.Chapters.FirstOrDefault(c => c.Id == currentChapterId);
if (currentChapter == null)
{
// Handle the case where the current chapter is not found
return -1;
}
var currentVolumeIndex = volumes.IndexOf(currentVolume);
var chapterId = -1;
if (currentVolume.IsSpecial())
{
// Handle specials by sorting on their Range
chapterId = GetNextSpecialChapter(currentVolume, currentChapter);
return chapterId;
}
if (currentVolume.IsLooseLeaf()) if (currentVolume.IsLooseLeaf())
{ {
// Handle specials by sorting on their Filename aka Range // Handle loose-leaf chapters
var chapterId = GetNextChapterId(currentVolume.Chapters.OrderByNatural(x => x.Range), currentChapter.Range, dto => dto.Range); chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => x.SortOrder),
currentChapter.SortOrder,
dto => dto.SortOrder);
if (chapterId > 0) return chapterId; if (chapterId > 0) return chapterId;
// Check specials next, as that is the order
if (currentVolumeIndex + 1 >= volumes.Count) return -1; // There are no special volumes, so there is nothing
var specialVolume = volumes[currentVolumeIndex + 1];
if (!specialVolume.IsSpecial()) return -1;
return specialVolume.Chapters.OrderByNatural(c => c.Range).FirstOrDefault()?.Id ?? -1;
} }
var next = false; // Check within the current volume if the next chapter within it can be next
foreach (var volume in volumes) var chapters = currentVolume.Chapters.OrderBy(c => c.MinNumber).ToList();
var currentChapterIndex = chapters.IndexOf(currentChapter);
if (currentChapterIndex < chapters.Count - 1)
{ {
var volumeNumbersMatch = volume.Name == currentVolume.Name; return chapters[currentChapterIndex + 1].Id;
if (volumeNumbersMatch && volume.Chapters.Count > 1)
{
// Handle Chapters within current Volume
// In this case, i need 0 first because 0 represents a full volume file.
var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => x.Number.AsFloat(), _chapterSortComparer),
currentChapter.Range, dto => dto.Range);
if (chapterId > 0) return chapterId;
next = true;
continue;
}
if (volumeNumbersMatch)
{
next = true;
continue;
}
if (!next) continue;
// Handle Chapters within next Volume
// ! When selecting the chapter for the next volume, we need to make sure a c0 comes before a c1+
var chapters = volume.Chapters.OrderBy(x => x.Number.AsDouble(), _chapterSortComparer).ToList();
if (currentChapter.Number.Equals(Parser.DefaultChapter) && chapters[^1].Number.Equals(Parser.DefaultChapter))
{
// We need to handle an extra check if the current chapter is the last special, as we should return -1
if (currentChapter.IsSpecial) return -1;
return chapters.Last().Id;
}
var firstChapter = chapters.FirstOrDefault();
if (firstChapter == null) break;
var isSpecial = firstChapter.IsSpecial || currentChapter.IsSpecial;
if (isSpecial)
{
var chapterId = GetNextChapterId(volume.Chapters.OrderByNatural(x => x.Number),
currentChapter.Range, dto => dto.Range);
if (chapterId > 0) return chapterId;
} else if (firstChapter.Number.AsDouble() >= currentChapter.Number.AsDouble()) return firstChapter.Id;
// If we are the last chapter and next volume is there, we should try to use it (unless it's volume 0)
else if (firstChapter.Number.AsDouble() == Parser.DefaultChapterNumber) return firstChapter.Id;
// If on last volume AND there are no specials left, then let's return -1
var anySpecials = volumes.Where(v => $"{v.MinNumber}" == Parser.LooseLeafVolume)
.SelectMany(v => v.Chapters.Where(c => c.IsSpecial)).Any();
if (!currentVolume.IsLooseLeaf() && !anySpecials)
{
return -1;
}
} }
// Check within the current Volume
chapterId = GetNextChapterId(chapters, currentChapter.SortOrder, dto => dto.SortOrder);
if (chapterId > 0) return chapterId;
// Now check the next volume
// If we are the last volume and we didn't find any next volume, loop back to volume 0 and give the first chapter var nextVolumeIndex = currentVolumeIndex + 1;
// This has an added problem that it will loop up to the beginning always if (nextVolumeIndex < volumes.Count)
// Should I change this to Max number? volumes.LastOrDefault()?.Number -> volumes.Max(v => v.Number)
if (!currentVolume.IsLooseLeaf() && currentVolume.MinNumber == volumes.LastOrDefault()?.MinNumber && volumes.Count > 1)
{ {
var chapterVolume = volumes.FirstOrDefault(); // Get the first chapter from the next volume
if (chapterVolume == null || !chapterVolume.IsLooseLeaf()) return -1; chapterId = volumes[nextVolumeIndex].Chapters.MinBy(c => c.MinNumber, _chapterSortComparerForInChapterSorting)?.Id ?? -1;
return chapterId;
}
// 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 // We are the last volume, so we need to check loose leaf
// var orderedVolumes = volumes.OrderBy(v => v.Number, SortComparerZeroLast.Default).ToList(); if (currentVolumeIndex == volumes.Count - 1)
// if (currentVolume.Number == orderedVolumes.FirstOrDefault().Number) {
// { // Try to find the first loose-leaf chapter in this volume
// // We can move into loose leaf chapters var firstLooseLeafChapter = volumes.WhereLooseLeaf().FirstOrDefault()?.Chapters.MinBy(c => c.MinNumber, _chapterSortComparerForInChapterSorting);
// //var firstLooseLeaf = volumes.LastOrDefault().Chapters.MinBy(x => x.Number.AsDouble(), _chapterSortComparer); if (firstLooseLeafChapter != null)
// var nextChapterId = GetNextChapterId( {
// volumes.LastOrDefault().Chapters.OrderBy(x => x.Number.AsDouble(), _chapterSortComparer), return firstLooseLeafChapter.Id;
// "0", dto => dto.Range); }
// // CHECK if we need a IsSpecial check
// if (nextChapterId > 0) return nextChapterId;
// }
var firstChapter = chapterVolume.Chapters.MinBy(x => x.Number.AsDouble(), _chapterSortComparer);
if (firstChapter == null) return -1;
return firstChapter.Id;
} }
return -1; return -1;
} }
/// <summary> /// <summary>
/// Tries to find the prev logical Chapter /// Tries to find the prev logical Chapter
/// </summary> /// </summary>
/// <example> /// <example>
/// V1 ← V2 ← V3 chapter 0 ← V3 chapter 10 ← V0 chapter 1 ← V0 chapter 2 ← SP 01 ← SP 02 /// V1 ← V2 ← V3 chapter 0 ← V3 chapter 10 ← (V0 chapter 1 ← V0 chapter 2 ← SP 01 ← SP 02)
/// </example> /// </example>
/// <param name="seriesId"></param> /// <param name="seriesId"></param>
/// <param name="volumeId"></param> /// <param name="volumeId"></param>
@ -473,52 +462,76 @@ public class ReaderService : IReaderService
/// <returns>-1 if nothing can be found</returns> /// <returns>-1 if nothing can be found</returns>
public async Task<int> GetPrevChapterIdAsync(int seriesId, int volumeId, int currentChapterId, int userId) public async Task<int> GetPrevChapterIdAsync(int seriesId, int volumeId, int currentChapterId, int userId)
{ {
var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId)).Reverse().ToList(); var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId)).ToList();
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.IsLooseLeaf()) var chapterId = -1;
if (currentVolume.IsSpecial())
{ {
var chapterId = GetNextChapterId(currentVolume.Chapters.OrderByNatural(x => x.Range).Reverse(), currentChapter.Range, // Check within Specials, if not set the currentVolume to Loose Leaf
dto => dto.Range); chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => x.SortOrder).Reverse(),
currentChapter.SortOrder,
dto => dto.SortOrder);
if (chapterId > 0) return chapterId; if (chapterId > 0) return chapterId;
currentVolume = volumes.FirstOrDefault(v => v.IsLooseLeaf());
} }
var next = false; if (currentVolume != null && currentVolume.IsLooseLeaf())
foreach (var volume in volumes)
{ {
if (volume.MinNumber == currentVolume.MinNumber) // If loose leaf, handle within the loose leaf. If not there, then set currentVolume to volumes.Last() where not LooseLeaf or Special
{ var currentVolumeChapters = currentVolume.Chapters.OrderBy(x => x.SortOrder).ToList();
var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => x.Number.AsDouble(), _chapterSortComparerForInChapterSorting).Reverse(), chapterId = GetPrevChapterId(currentVolumeChapters,
currentChapter.Range, dto => dto.Range); currentChapter.SortOrder, dto => dto.SortOrder, c => c.Id);
if (chapterId > 0) return chapterId; if (chapterId > 0) return chapterId;
next = true; // When the diff between volumes is more than 1, we need to explicitly tell that next volume is our use case currentVolume = volumes.FindLast(v => !v.IsLooseLeaf() && !v.IsSpecial());
continue; if (currentVolume != null) return currentVolume.Chapters.OrderBy(x => x.SortOrder).Last()?.Id ?? -1;
}
if (next)
{
if (currentVolume.MinNumber - 1 == Parser.LooseLeafVolumeNumber) 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.MinNumber); // When we started as a special and there was no loose leafs, reset the currentVolume
if (currentVolume.IsLooseLeaf() && currentVolume.MinNumber != lastVolume?.MinNumber && lastVolume?.Chapters.Count > 1) if (currentVolume == null)
{ {
var lastChapter = lastVolume.Chapters.MaxBy(x => x.Number.AsDouble(), _chapterSortComparerForInChapterSorting); currentVolume = volumes.FirstOrDefault(v => !v.IsLooseLeaf() && !v.IsSpecial());
if (lastChapter == null) return -1; if (currentVolume == null) return -1;
return lastChapter.Id; return currentVolume.Chapters.OrderBy(x => x.SortOrder).Last()?.Id ?? -1;
} }
// At this point, only need to check within the current Volume else move 1 level back
// Check current volume
chapterId = GetPrevChapterId(currentVolume.Chapters.OrderBy(x => x.SortOrder),
currentChapter.SortOrder, dto => dto.SortOrder, c => c.Id);
if (chapterId > 0) return chapterId;
var currentVolumeIndex = volumes.IndexOf(currentVolume);
if (currentVolumeIndex == 0) return -1;
currentVolume = volumes[currentVolumeIndex - 1];
if (currentVolume.IsLooseLeaf() || currentVolume.IsSpecial()) return -1;
chapterId = currentVolume.Chapters.OrderBy(x => x.SortOrder).Last().Id;
if (chapterId > 0) return chapterId;
return -1;
}
private static int GetPrevChapterId<T>(IEnumerable<T> source, float currentValue, Func<T, float> selector, Func<T, int> idSelector)
{
var sortedSource = source.OrderBy(selector).ToList();
var currentChapterIndex = sortedSource.FindIndex(x => selector(x).Is(currentValue));
if (currentChapterIndex > 0)
{
return idSelector(sortedSource[currentChapterIndex - 1]);
}
// There is no previous chapter
return -1; return -1;
} }
/// <summary> /// <summary>
/// Finds the chapter to continue reading from. If a chapter has progress and not complete, return that. If not, progress in the /// Finds the chapter to continue reading from. If a chapter has progress and not complete, return that. If not, progress in the
/// ordering (Volumes -> Loose Chapters -> Special) to find next chapter. If all are read, return first in order for series. /// ordering (Volumes -> Loose Chapters -> Annuals -> Special) to find next chapter. If all are read, return first in order for series.
/// </summary> /// </summary>
/// <param name="seriesId"></param> /// <param name="seriesId"></param>
/// <param name="userId"></param> /// <param name="userId"></param>
@ -527,28 +540,42 @@ public class ReaderService : IReaderService
{ {
var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId)).ToList(); var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId)).ToList();
if (!await _unitOfWork.AppUserProgressRepository.AnyUserProgressForSeriesAsync(seriesId, userId)) var anyUserProgress =
{ await _unitOfWork.AppUserProgressRepository.AnyUserProgressForSeriesAsync(seriesId, userId);
// I think i need a way to sort volumes last
var chapters = volumes.OrderBy(v => v.MinNumber, _chapterSortComparer).First().Chapters
.OrderBy(c => c.Number.AsFloat())
.ToList();
// If there are specials, then return the first Non-special if (!anyUserProgress)
if (chapters.Exists(c => c.IsSpecial)) {
{ // I think i need a way to sort volumes last
var firstChapter = chapters.FirstOrDefault(c => !c.IsSpecial); volumes = volumes.OrderBy(v => v.MinNumber, _chapterSortComparerSpecialsLast).ToList();
if (firstChapter == null)
{
// If there is no non-special chapter, then return first chapter
return chapters[0];
}
return firstChapter; // Check if we have a non-loose leaf volume
} var nonLooseLeafNonSpecialVolume = volumes.Find(v => !v.IsLooseLeaf() && !v.IsSpecial());
// Else use normal logic if (nonLooseLeafNonSpecialVolume != null)
return chapters[0]; {
} return nonLooseLeafNonSpecialVolume.Chapters.MinBy(c => c.SortOrder);
}
// We only have a loose leaf or Special left
var chapters = volumes.First(v => v.IsLooseLeaf() || v.IsSpecial()).Chapters
.OrderBy(c => c.SortOrder)
.ToList();
// If there are specials, then return the first Non-special
if (chapters.Exists(c => c.IsSpecial))
{
var firstChapter = chapters.Find(c => !c.IsSpecial);
if (firstChapter == null)
{
// If there is no non-special chapter, then return first chapter
return chapters[0];
}
return firstChapter;
}
// Else use normal logic
return chapters[0];
}
// 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
@ -559,13 +586,13 @@ public class ReaderService : IReaderService
// NOTE: If volume 1 has chapter 1 and volume 2 is just chapter 0 due to being a full volume file, then this fails // NOTE: If volume 1 has chapter 1 and volume 2 is just chapter 0 due to being a full volume file, then this fails
// If there are any volumes that have progress, return those. If not, move on. // If there are any volumes that have progress, return those. If not, move on.
var currentlyReadingChapter = volumeChapters var currentlyReadingChapter = volumeChapters
.OrderBy(c => c.Number.AsDouble(), _chapterSortComparer) .OrderBy(c => c.MinNumber, _chapterSortComparerDefaultLast)
.FirstOrDefault(chapter => chapter.PagesRead < chapter.Pages && chapter.PagesRead > 0); .FirstOrDefault(chapter => chapter.PagesRead < chapter.Pages && chapter.PagesRead > 0);
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.MinNumber, SortComparerZeroLast.Default) return FindNextReadingChapter(volumes.OrderBy(v => v.MinNumber, _chapterSortComparerDefaultLast)
.SelectMany(v => v.Chapters.OrderBy(c => c.Number.AsDouble())) .SelectMany(v => v.Chapters.OrderBy(c => c.SortOrder))
.ToList()); .ToList());
} }
@ -606,7 +633,7 @@ public class ReaderService : IReaderService
} }
private static int GetNextChapterId(IEnumerable<ChapterDto> chapters, string currentChapterNumber, Func<ChapterDto, string> accessor) private static int GetNextChapterId(IEnumerable<ChapterDto> chapters, float currentChapterNumber, Func<ChapterDto, float> accessor)
{ {
var next = false; var next = false;
var chaptersList = chapters.ToList(); var chaptersList = chapters.ToList();
@ -636,8 +663,8 @@ public class ReaderService : IReaderService
foreach (var volume in volumes.OrderBy(v => v.MinNumber)) 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 && c.MaxNumber <= chapterNumber)
.OrderBy(c => c.Number.AsFloat()); .OrderBy(c => c.MinNumber);
await MarkChaptersAsRead(user, volume.SeriesId, chapters.ToList()); await MarkChaptersAsRead(user, volume.SeriesId, chapters.ToList());
} }
} }

View file

@ -57,7 +57,7 @@ public class ReadingListService : IReadingListService
private readonly IUnitOfWork _unitOfWork; private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<ReadingListService> _logger; private readonly ILogger<ReadingListService> _logger;
private readonly IEventHub _eventHub; private readonly IEventHub _eventHub;
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = ChapterSortComparerZeroFirst.Default; private readonly ChapterSortComparerDefaultFirst _chapterSortComparerForInChapterSorting = ChapterSortComparerDefaultFirst.Default;
private static readonly Regex JustNumbers = new Regex(@"^\d+$", RegexOptions.Compiled | RegexOptions.IgnoreCase, private static readonly Regex JustNumbers = new Regex(@"^\d+$", RegexOptions.Compiled | RegexOptions.IgnoreCase,
Parser.RegexTimeout); Parser.RegexTimeout);
@ -391,8 +391,8 @@ public class ReadingListService : IReadingListService
var existingChapterExists = readingList.Items.Select(rli => rli.ChapterId).ToHashSet(); var existingChapterExists = readingList.Items.Select(rli => rli.ChapterId).ToHashSet();
var chaptersForSeries = (await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds, ChapterIncludes.Volumes)) var chaptersForSeries = (await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds, ChapterIncludes.Volumes))
.OrderBy(c => Parser.MinNumberFromRange(c.Volume.Name)) .OrderBy(c => c.Volume.MinNumber)
.ThenBy(x => x.Number.AsDouble(), _chapterSortComparerForInChapterSorting) .ThenBy(x => x.MinNumber, _chapterSortComparerForInChapterSorting)
.ToList(); .ToList();
var index = readingList.Items.Count == 0 ? 0 : lastOrder + 1; var index = readingList.Items.Count == 0 ? 0 : lastOrder + 1;
@ -647,9 +647,9 @@ public class ReadingListService : IReadingListService
// We need to handle chapter 0 or empty string when it's just a volume // We need to handle chapter 0 or empty string when it's just a volume
var bookNumber = string.IsNullOrEmpty(book.Number) var bookNumber = string.IsNullOrEmpty(book.Number)
? Parser.DefaultChapter ? Parser.DefaultChapterNumber
: book.Number; : float.Parse(book.Number);
var chapter = matchingVolume.Chapters.FirstOrDefault(c => c.Number == bookNumber); var chapter = matchingVolume.Chapters.FirstOrDefault(c => c.MinNumber.Is(bookNumber));
if (chapter == null) if (chapter == null)
{ {
importSummary.Results.Add(new CblBookResult(book) importSummary.Results.Add(new CblBookResult(book)

View file

@ -59,7 +59,7 @@ public class SeriesService : ISeriesService
{ {
ExpectedDate = null, ExpectedDate = null,
ChapterNumber = 0, ChapterNumber = 0,
VolumeNumber = 0 VolumeNumber = Parser.LooseLeafVolumeNumber
}; };
public SeriesService(IUnitOfWork unitOfWork, IEventHub eventHub, ITaskScheduler taskScheduler, public SeriesService(IUnitOfWork unitOfWork, IEventHub eventHub, ITaskScheduler taskScheduler,
@ -81,21 +81,21 @@ public class SeriesService : ISeriesService
public static Chapter? GetFirstChapterForMetadata(Series series) public static Chapter? GetFirstChapterForMetadata(Series series)
{ {
var sortedVolumes = series.Volumes var sortedVolumes = series.Volumes
.Where(v => float.TryParse(v.Name, CultureInfo.InvariantCulture, out var parsedValue) && parsedValue != Parser.LooseLeafVolumeNumber) .Where(v => v.MinNumber.IsNot(Parser.LooseLeafVolumeNumber))
.OrderBy(v => float.TryParse(v.Name, CultureInfo.InvariantCulture, out var parsedValue) ? parsedValue : float.MaxValue); .OrderBy(v => v.MinNumber);
var minVolumeNumber = sortedVolumes.MinBy(v => v.MinNumber); var minVolumeNumber = sortedVolumes.MinBy(v => v.MinNumber);
var allChapters = series.Volumes var allChapters = series.Volumes
.SelectMany(v => v.Chapters.OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default)) .SelectMany(v => v.Chapters.OrderBy(c => c.MinNumber, ChapterSortComparerDefaultLast.Default))
.ToList(); .ToList();
var minChapter = allChapters var minChapter = allChapters
.FirstOrDefault(); .FirstOrDefault();
if (minVolumeNumber != null && minChapter != null && float.TryParse(minChapter.Number, CultureInfo.InvariantCulture, out var chapNum) && if (minVolumeNumber != null && minChapter != null &&
(chapNum >= minVolumeNumber.MinNumber || chapNum == Parser.DefaultChapterNumber)) (minChapter.MinNumber >= minVolumeNumber.MinNumber || minChapter.MinNumber.Is(Parser.DefaultChapterNumber)))
{ {
return minVolumeNumber.Chapters.MinBy(c => c.Number.AsFloat(), ChapterSortComparer.Default); return minVolumeNumber.Chapters.MinBy(c => c.MinNumber, ChapterSortComparerDefaultLast.Default);
} }
return minChapter; return minChapter;
@ -481,74 +481,63 @@ public class SeriesService : ISeriesService
var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId); var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId);
var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId)) var bookTreatment = libraryType is LibraryType.Book or LibraryType.LightNovel;
.OrderBy(v => Parser.MinNumberFromRange(v.Name)) var volumeLabel = await _localizationService.Translate(userId, "volume-num", string.Empty);
.ToList(); var volumes = await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId);
// For books, the Name of the Volume is remapped to the actual name of the book, rather than Volume number. // For books, the Name of the Volume is remapped to the actual name of the book, rather than Volume number.
var processedVolumes = new List<VolumeDto>(); var processedVolumes = new List<VolumeDto>();
if (libraryType is LibraryType.Book or LibraryType.LightNovel) foreach (var volume in volumes)
{ {
var volumeLabel = await _localizationService.Translate(userId, "volume-num", string.Empty); if (volume.IsLooseLeaf() || volume.IsSpecial())
foreach (var volume in volumes) {
continue;
}
volume.Chapters = volume.Chapters
.OrderBy(d => d.MinNumber, ChapterSortComparerDefaultLast.Default)
.ToList();
if (RenameVolumeName(volume, libraryType, volumeLabel) || (bookTreatment && !volume.IsSpecial()))
{ {
volume.Chapters = volume.Chapters
.OrderBy(d => d.Number.AsDouble(), ChapterSortComparer.Default)
.ToList();
var firstChapter = volume.Chapters.First();
// On Books, skip volumes that are specials, since these will be shown
if (firstChapter.IsSpecial) continue;
RenameVolumeName(firstChapter, volume, libraryType, volumeLabel);
processedVolumes.Add(volume); processedVolumes.Add(volume);
} }
} }
else
{
processedVolumes = volumes.Where(v => v.MinNumber > 0).ToList();
processedVolumes.ForEach(v =>
{
v.Name = $"Volume {v.Name}";
v.Chapters = v.Chapters.OrderBy(d => d.Number.AsDouble(), ChapterSortComparer.Default).ToList();
});
}
var specials = new List<ChapterDto>(); var specials = new List<ChapterDto>();
var chapters = volumes.SelectMany(v => v.Chapters.Select(c => // Why isn't this doing a check if chapter is not special as it wont get included
{ var chapters = volumes
if (v.IsLooseLeaf()) return c; .SelectMany(v => v.Chapters
c.VolumeTitle = v.Name; .Select(c =>
return c; {
}).OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default)).ToList(); if (v.IsLooseLeaf()) return c;
c.VolumeTitle = v.Name;
return c;
})
.OrderBy(c => c.SortOrder))
.ToList();
foreach (var chapter in chapters) foreach (var chapter in chapters)
{ {
chapter.Title = await FormatChapterTitle(userId, chapter, libraryType);
if (!chapter.IsSpecial) continue;
if (!string.IsNullOrEmpty(chapter.TitleName)) chapter.Title = chapter.TitleName; if (!string.IsNullOrEmpty(chapter.TitleName)) chapter.Title = chapter.TitleName;
else chapter.Title = await FormatChapterTitle(userId, chapter, libraryType);
if (!chapter.IsSpecial) continue;
specials.Add(chapter); specials.Add(chapter);
} }
// Don't show chapter 0 (aka single volume chapters) in the Chapters tab or books that are just single numbers (they show as volumes) // Don't show chapter 0 (aka single volume chapters) in the Chapters tab or books that are just single numbers (they show as volumes)
IEnumerable<ChapterDto> retChapters; IEnumerable<ChapterDto> retChapters = bookTreatment ? Array.Empty<ChapterDto>() : chapters.Where(ShouldIncludeChapter);
if (libraryType is LibraryType.Book or LibraryType.LightNovel)
{
retChapters = Array.Empty<ChapterDto>();
} else
{
retChapters = chapters
.Where(ShouldIncludeChapter);
}
var storylineChapters = volumes var storylineChapters = volumes
.WhereLooseLeaf() .WhereLooseLeaf()
.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.SortOrder)
.ToList(); .ToList();
// When there's chapters without a volume number revert to chapter sorting only as opposed to volume then chapter // When there's chapters without a volume number revert to chapter sorting only as opposed to volume then chapter
if (storylineChapters.Any()) { if (storylineChapters.Count > 0) {
retChapters = retChapters.OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default); retChapters = retChapters.OrderBy(c => c.SortOrder, ChapterSortComparerDefaultLast.Default);
} }
return new SeriesDetailDto return new SeriesDetailDto
@ -569,35 +558,35 @@ public class SeriesService : ISeriesService
/// <returns></returns> /// <returns></returns>
private static bool ShouldIncludeChapter(ChapterDto chapter) private static bool ShouldIncludeChapter(ChapterDto chapter)
{ {
return !chapter.IsSpecial && !chapter.Number.Equals(Parser.DefaultChapter); return !chapter.IsSpecial && chapter.MinNumber.IsNot(Parser.DefaultChapterNumber);
} }
public static void RenameVolumeName(ChapterDto firstChapter, VolumeDto volume, LibraryType libraryType, string volumeLabel = "Volume") public static bool RenameVolumeName(VolumeDto volume, LibraryType libraryType, string volumeLabel = "Volume")
{ {
// TODO: Move this into DB // TODO: Move this into DB (not sure how because of localization and lookups)
if (libraryType is LibraryType.Book or LibraryType.LightNovel) if (libraryType is LibraryType.Book or LibraryType.LightNovel)
{ {
var firstChapter = volume.Chapters.First();
// On Books, skip volumes that are specials, since these will be shown
if (firstChapter.IsSpecial) return false;
if (string.IsNullOrEmpty(firstChapter.TitleName)) if (string.IsNullOrEmpty(firstChapter.TitleName))
{ {
if (firstChapter.Range.Equals(Parser.LooseLeafVolume)) return; if (firstChapter.Range.Equals(Parser.LooseLeafVolume)) return false;
var title = Path.GetFileNameWithoutExtension(firstChapter.Range); var title = Path.GetFileNameWithoutExtension(firstChapter.Range);
if (string.IsNullOrEmpty(title)) return; if (string.IsNullOrEmpty(title)) return false;
volume.Name += $" - {title}"; volume.Name += $" - {title}"; // OPDS smart list 7 (just pdfs) triggered this
} }
else if (volume.Name != Parser.LooseLeafVolume) else if (!volume.IsLooseLeaf())
{ {
// If the titleName has Volume inside it, let's just send that back? // If the titleName has Volume inside it, let's just send that back?
volume.Name += $" - {firstChapter.TitleName}"; volume.Name = firstChapter.TitleName;
} }
// else
// {
// volume.Name += $"";
// }
return; return true;
} }
volume.Name = $"{volumeLabel} {volume.Name}".Trim(); volume.Name = $"{volumeLabel.Trim()} {volume.Name}".Trim();
return true;
} }
@ -783,16 +772,15 @@ public class SeriesService : ISeriesService
: (DateTime?)null; : (DateTime?)null;
// For number and volume number, we need the highest chapter, not the latest created // For number and volume number, we need the highest chapter, not the latest created
var lastChapter = chapters.MaxBy(c => c.Number.AsFloat())!; var lastChapter = chapters.MaxBy(c => c.MaxNumber)!;
float.TryParse(lastChapter.Number, NumberStyles.Number, CultureInfo.InvariantCulture, var lastChapterNumber = lastChapter.MaxNumber;
out var lastChapterNumber);
var lastVolumeNum = chapters.Select(c => c.Volume.MinNumber).Max(); var lastVolumeNum = chapters.Select(c => c.Volume.MinNumber).Max();
var result = new NextExpectedChapterDto var result = new NextExpectedChapterDto
{ {
ChapterNumber = 0, ChapterNumber = 0,
VolumeNumber = 0, VolumeNumber = Parser.LooseLeafVolumeNumber,
ExpectedDate = nextChapterExpected, ExpectedDate = nextChapterExpected,
Title = string.Empty Title = string.Empty
}; };

View file

@ -336,7 +336,7 @@ public class StatisticService : IStatisticService
LibraryId = u.LibraryId, LibraryId = u.LibraryId,
ReadDate = u.LastModified, ReadDate = u.LastModified,
ChapterId = u.ChapterId, ChapterId = u.ChapterId,
ChapterNumber = _context.Chapter.Single(c => c.Id == u.ChapterId).Number ChapterNumber = _context.Chapter.Single(c => c.Id == u.ChapterId).MinNumber
}) })
.OrderByDescending(d => d.ReadDate) .OrderByDescending(d => d.ReadDate)
.ToListAsync(); .ToListAsync();

View file

@ -14,10 +14,11 @@ using AutoMapper;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace API.Services; namespace API.Services;
#nullable enable
public interface ITachiyomiService public interface ITachiyomiService
{ {
Task<ChapterDto?> GetLatestChapter(int seriesId, int userId); Task<TachiyomiChapterDto?> GetLatestChapter(int seriesId, int userId);
Task<bool> MarkChaptersUntilAsRead(AppUser userWithProgress, int seriesId, float chapterNumber); Task<bool> MarkChaptersUntilAsRead(AppUser userWithProgress, int seriesId, float chapterNumber);
} }
@ -51,7 +52,7 @@ public class TachiyomiService : ITachiyomiService
/// If its a chapter, return the chapterDto as is. /// If its a chapter, return the chapterDto as is.
/// If it's a volume, the volume number gets returned in the 'Number' attribute of a chapterDto encoded. /// If it's a volume, the volume number gets returned in the 'Number' attribute of a chapterDto encoded.
/// The volume number gets divided by 10,000 because that's how Tachiyomi interprets volumes</returns> /// The volume number gets divided by 10,000 because that's how Tachiyomi interprets volumes</returns>
public async Task<ChapterDto?> GetLatestChapter(int seriesId, int userId) public async Task<TachiyomiChapterDto?> GetLatestChapter(int seriesId, int userId)
{ {
var currentChapter = await _readerService.GetContinuePoint(seriesId, userId); var currentChapter = await _readerService.GetContinuePoint(seriesId, userId);
@ -74,50 +75,48 @@ public class TachiyomiService : ITachiyomiService
{ {
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.MinNumber, ChapterSortComparerDefaultFirst.Default)
.Last()); .Last());
if (volumeChapter.Number == Parser.LooseLeafVolume)
if (volumeChapter.MinNumber.Is(Parser.LooseLeafVolumeNumber))
{ {
var volume = volumes.First(v => v.Id == volumeChapter.VolumeId); var volume = volumes.First(v => v.Id == volumeChapter.VolumeId);
return new ChapterDto() return CreateTachiyomiChapterDto(volume.MinNumber);
{
// 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.MinNumber / 10_000f).ToString("R", EnglishCulture)
};
} }
return new ChapterDto() return CreateTachiyomiChapterDto(volumeChapter.MinNumber);
{
Number = (int.Parse(volumeChapter.Number) / 10_000f).ToString("R", EnglishCulture)
};
} }
var lastChapter = looseLeafChapterVolume.Chapters var lastChapter = looseLeafChapterVolume.Chapters
.OrderBy(c => double.Parse(c.Number, CultureInfo.InvariantCulture), ChapterSortComparer.Default) .OrderBy(c => c.MinNumber, ChapterSortComparerDefaultLast.Default)
.Last(); .Last();
return _mapper.Map<ChapterDto>(lastChapter);
return _mapper.Map<TachiyomiChapterDto>(lastChapter);
} }
// There is progress, we now need to figure out the highest volume or chapter and return that. // There is progress, we now need to figure out the highest volume or chapter and return that.
var prevChapter = (await _unitOfWork.ChapterRepository.GetChapterDtoAsync(prevChapterId))!; var prevChapter = (await _unitOfWork.ChapterRepository.GetChapterDtoAsync(prevChapterId))!;
var volumeWithProgress = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(prevChapter.VolumeId, userId); 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!.IsLooseLeaf() && volumeWithProgress.Chapters.Count == 1) if (!volumeWithProgress.IsLooseLeaf() && 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 CreateTachiyomiChapterDto(volumeWithProgress.MinNumber);
{
// 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.MinNumber / 10_000f).ToString("R", EnglishCulture)
};
} }
// Progress is just on a chapter, return as is // Progress is just on a chapter, return as is
return prevChapter; return _mapper.Map<TachiyomiChapterDto>(prevChapter);
}
private static TachiyomiChapterDto CreateTachiyomiChapterDto(float number)
{
return new TachiyomiChapterDto()
{
// 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 = (number / 10_000f).ToString("R", EnglishCulture)
};
} }
/// <summary> /// <summary>

View file

@ -365,12 +365,68 @@ public class ParseScannedFiles
foreach (var series in scannedSeries.Keys) foreach (var series in scannedSeries.Keys)
{ {
if (scannedSeries[series].Count > 0 && processSeriesInfos != null) if (scannedSeries[series].Count <= 0 || processSeriesInfos == null) continue;
UpdateSortOrder(scannedSeries, series);
await processSeriesInfos.Invoke(new Tuple<bool, IList<ParserInfo>>(false, scannedSeries[series]));
}
}
}
private void UpdateSortOrder(ConcurrentDictionary<ParsedSeries, List<ParserInfo>> scannedSeries, ParsedSeries series)
{
try
{
// Set the Sort order per Volume
var volumes = scannedSeries[series].GroupBy(info => info.Volumes);
foreach (var volume in volumes)
{
var infos = scannedSeries[series].Where(info => info.Volumes == volume.Key).ToList();
IList<ParserInfo> chapters;
var specialTreatment = infos.TrueForAll(info => info.IsSpecial);
if (specialTreatment)
{ {
await processSeriesInfos.Invoke(new Tuple<bool, IList<ParserInfo>>(false, scannedSeries[series])); chapters = infos
.OrderBy(info => info.SpecialIndex)
.ToList();
}
else
{
chapters = infos
.OrderByNatural(info => info.Chapters)
.ToList();
}
var counter = 0f;
var prevIssue = string.Empty;
foreach (var chapter in chapters)
{
if (float.TryParse(chapter.Chapters, out var parsedChapter))
{
counter = parsedChapter;
if (!string.IsNullOrEmpty(prevIssue) && parsedChapter.Is(float.Parse(prevIssue)))
{
// Bump by 0.1
counter += 0.1f;
}
chapter.IssueOrder = counter;
prevIssue = $"{parsedChapter}";
}
else
{
chapter.IssueOrder = counter;
counter++;
prevIssue = chapter.Chapters;
}
} }
} }
} }
catch (Exception ex)
{
_logger.LogError(ex, "There was an issue setting IssueOrder");
}
} }
/// <summary> /// <summary>

View file

@ -96,6 +96,7 @@ public class DefaultParser : IDefaultParser
if (Parser.HasSpecialMarker(fileName)) if (Parser.HasSpecialMarker(fileName))
{ {
ret.IsSpecial = true; ret.IsSpecial = true;
ret.SpecialIndex = Parser.ParseSpecialIndex(fileName);
ret.Chapters = Parser.DefaultChapter; ret.Chapters = Parser.DefaultChapter;
ret.Volumes = Parser.LooseLeafVolume; ret.Volumes = Parser.LooseLeafVolume;
@ -113,6 +114,12 @@ public class DefaultParser : IDefaultParser
ret.Series = ret.Series.Substring(0, ret.Series.Length - ".pdf".Length); ret.Series = ret.Series.Substring(0, ret.Series.Length - ".pdf".Length);
} }
// v0.8.x: Introducing a change where Specials will go in a separate Volume with a reserved number
if (ret.IsSpecial)
{
ret.Volumes = $"{Parser.SpecialVolumeNumber}";
}
return ret.Series == string.Empty ? null : ret; return ret.Series == string.Empty ? null : ret;
} }

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -12,10 +13,16 @@ namespace API.Services.Tasks.Scanner.Parser;
public static class Parser public static class Parser
{ {
// NOTE: If you change this, don't forget to change in the UI (see Series Detail) // NOTE: If you change this, don't forget to change in the UI (see Series Detail)
public const string DefaultChapter = "0"; // -2147483648 public const string DefaultChapter = "-100000"; // -2147483648
public const string LooseLeafVolume = "0"; public const string LooseLeafVolume = "-100000";
public const int DefaultChapterNumber = 0; public const int DefaultChapterNumber = -100_000;
public const int LooseLeafVolumeNumber = 0; public const int LooseLeafVolumeNumber = -100_000;
/// <summary>
/// The Volume Number of Specials to reside in
/// </summary>
public const int SpecialVolumeNumber = 100_000;
public const string SpecialVolume = "100000";
public static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(500); public static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(500);
public const string ImageFileExtensions = @"^(\.png|\.jpeg|\.jpg|\.webp|\.gif|\.avif)"; // Don't forget to update CoverChooser public const string ImageFileExtensions = @"^(\.png|\.jpeg|\.jpg|\.webp|\.gif|\.avif)"; // Don't forget to update CoverChooser
@ -678,6 +685,13 @@ public static class Parser
return SpecialMarkerRegex.IsMatch(filePath); return SpecialMarkerRegex.IsMatch(filePath);
} }
public static int ParseSpecialIndex(string filePath)
{
var match = SpecialMarkerRegex.Match(filePath).Value.Replace("SP", string.Empty);
if (string.IsNullOrEmpty(match)) return 0;
return int.Parse(match);
}
public static bool IsMangaSpecial(string filePath) public static bool IsMangaSpecial(string filePath)
{ {
filePath = ReplaceUnderscores(filePath); filePath = ReplaceUnderscores(filePath);
@ -944,35 +958,52 @@ public static class Parser
{ {
try try
{ {
if (!Regex.IsMatch(range, @"^[\d\-.]+$", MatchOptions, RegexTimeout)) // Check if the range string is not null or empty
if (string.IsNullOrEmpty(range) || !Regex.IsMatch(range, @"^[\d\-.]+$", MatchOptions, RegexTimeout))
{ {
return (float) 0.0; return 0.0f;
} }
var tokens = range.Replace("_", string.Empty).Split("-"); // Check if there is a range or not
return tokens.Min(t => t.AsFloat()); if (Regex.IsMatch(range, @"\d-{1}\d"))
{
var tokens = range.Replace("_", string.Empty).Split("-", StringSplitOptions.RemoveEmptyEntries);
return tokens.Min(t => t.AsFloat());
}
return float.Parse(range);
} }
catch catch (Exception)
{ {
return (float) 0.0; return 0.0f;
} }
} }
public static float MaxNumberFromRange(string range) public static float MaxNumberFromRange(string range)
{ {
try try
{ {
if (!Regex.IsMatch(range, @"^[\d\-.]+$", MatchOptions, RegexTimeout)) // Check if the range string is not null or empty
if (string.IsNullOrEmpty(range) || !Regex.IsMatch(range, @"^[\d\-.]+$", MatchOptions, RegexTimeout))
{ {
return (float) 0.0; return 0.0f;
} }
var tokens = range.Replace("_", string.Empty).Split("-"); // Check if there is a range or not
return tokens.Max(t => t.AsFloat()); if (Regex.IsMatch(range, @"\d-{1}\d"))
{
var tokens = range.Replace("_", string.Empty).Split("-", StringSplitOptions.RemoveEmptyEntries);
return tokens.Max(t => t.AsFloat());
}
return float.Parse(range);
} }
catch catch (Exception)
{ {
return (float) 0.0; return 0.0f;
} }
} }

View file

@ -60,6 +60,10 @@ public class ParserInfo
/// If the file contains no volume/chapter information or contains Special Keywords <see cref="Parser.MangaSpecialRegex"/> /// If the file contains no volume/chapter information or contains Special Keywords <see cref="Parser.MangaSpecialRegex"/>
/// </summary> /// </summary>
public bool IsSpecial { get; set; } public bool IsSpecial { get; set; }
/// <summary>
/// If the file has a Special Marker explicitly, this will contain the index
/// </summary>
public int SpecialIndex { get; set; } = 0;
/// <summary> /// <summary>
/// Used for specials or books, stores what the UI should show. /// Used for specials or books, stores what the UI should show.
@ -67,6 +71,12 @@ public class ParserInfo
/// </summary> /// </summary>
public string Title { get; set; } = string.Empty; public string Title { get; set; } = string.Empty;
/// <summary>
/// This can be filled in from ComicInfo.xml during scanning. Will update the SortOrder field on <see cref="Entities.Chapter"/>.
/// Falls back to Parsed Chapter number
/// </summary>
public float IssueOrder { get; set; }
/// <summary> /// <summary>
/// If the ParserInfo has the IsSpecial tag or both volumes and chapters are default aka 0 /// If the ParserInfo has the IsSpecial tag or both volumes and chapters are default aka 0
/// </summary> /// </summary>

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Data; using API.Data;
@ -219,14 +220,6 @@ public class ProcessSeries : IProcessSeries
_logger.LogCritical(ex, _logger.LogCritical(ex,
"[ScannerService] There was an issue writing to the database for series {SeriesName}", "[ScannerService] There was an issue writing to the database for series {SeriesName}",
series.Name); series.Name);
_logger.LogTrace("[ScannerService] Series Metadata Dump: {@Series}", series.Metadata);
_logger.LogTrace("[ScannerService] People Dump: {@People}", _people
.Select(p =>
new {p.Id, p.Name, SeriesMetadataIds =
p.SeriesMetadatas?.Select(m => m.Id),
ChapterMetadataIds =
p.ChapterMetadatas?.Select(m => m.Id)
.ToList()}));
await _eventHub.SendMessageAsync(MessageFactory.Error, await _eventHub.SendMessageAsync(MessageFactory.Error,
MessageFactory.ErrorEvent($"There was an issue writing to the DB for Series {series.OriginalName}", MessageFactory.ErrorEvent($"There was an issue writing to the DB for Series {series.OriginalName}",
@ -314,8 +307,8 @@ public class ProcessSeries : IProcessSeries
// The actual number of count's defined across all chapter's metadata // The actual number of count's defined across all chapter's metadata
series.Metadata.MaxCount = chapters.Max(chapter => chapter.Count); series.Metadata.MaxCount = chapters.Max(chapter => chapter.Count);
var maxVolume = series.Volumes.Max(v => (int) Parser.Parser.MaxNumberFromRange(v.Name)); var maxVolume = (int) series.Volumes.Max(v => v.MaxNumber);
var maxChapter = chapters.Max(c => (int) Parser.Parser.MaxNumberFromRange(c.Range)); var maxChapter = (int) chapters.Max(c => c.MaxNumber);
// Single books usually don't have a number in their Range (filename) // Single books usually don't have a number in their Range (filename)
if (series.Format == MangaFormat.Epub || series.Format == MangaFormat.Pdf && chapters.Count == 1) if (series.Format == MangaFormat.Epub || series.Format == MangaFormat.Pdf && chapters.Count == 1)
@ -544,10 +537,12 @@ public class ProcessSeries : IProcessSeries
Volume? volume; Volume? volume;
try try
{ {
volume = series.Volumes.SingleOrDefault(s => s.Name == volumeNumber); // With the Name change to be formatted, Name no longer working because Name returns "1" and volumeNumber is "1.0", so we use LookupName as the original
volume = series.Volumes.SingleOrDefault(s => s.LookupName == volumeNumber);
} }
catch (Exception ex) catch (Exception ex)
{ {
// TODO: Push this to UI in some way
if (!ex.Message.Equals("Sequence contains more than one matching element")) throw; if (!ex.Message.Equals("Sequence contains more than one matching element")) throw;
_logger.LogCritical("[ScannerService] Kavita found corrupted volume entries on {SeriesName}. Please delete the series from Kavita via UI and rescan", series.Name); _logger.LogCritical("[ScannerService] Kavita found corrupted volume entries on {SeriesName}. Please delete the series from Kavita via UI and rescan", series.Name);
throw new KavitaException( throw new KavitaException(
@ -561,7 +556,8 @@ public class ProcessSeries : IProcessSeries
series.Volumes.Add(volume); series.Volumes.Add(volume);
} }
volume.Name = volumeNumber; volume.LookupName = volumeNumber;
volume.Name = volume.GetNumberTitle();
_logger.LogDebug("[ScannerService] Parsing {SeriesName} - Volume {VolumeNumber}", series.Name, volume.Name); _logger.LogDebug("[ScannerService] Parsing {SeriesName} - Volume {VolumeNumber}", series.Name, volume.Name);
var infos = parsedInfos.Where(p => p.Volumes == volumeNumber).ToArray(); var infos = parsedInfos.Where(p => p.Volumes == volumeNumber).ToArray();
@ -586,7 +582,9 @@ public class ProcessSeries : IProcessSeries
} }
// Remove existing volumes that aren't in parsedInfos // Remove existing volumes that aren't in parsedInfos
var nonDeletedVolumes = series.Volumes.Where(v => parsedInfos.Select(p => p.Volumes).Contains(v.Name)).ToList(); var nonDeletedVolumes = series.Volumes
.Where(v => parsedInfos.Select(p => p.Volumes).Contains(v.LookupName))
.ToList();
if (series.Volumes.Count != nonDeletedVolumes.Count) if (series.Volumes.Count != nonDeletedVolumes.Count)
{ {
_logger.LogDebug("[ScannerService] Removed {Count} volumes from {SeriesName} where parsed infos were not mapping with volume name", _logger.LogDebug("[ScannerService] Removed {Count} volumes from {SeriesName} where parsed infos were not mapping with volume name",
@ -597,8 +595,9 @@ public class ProcessSeries : IProcessSeries
var file = volume.Chapters.FirstOrDefault()?.Files?.FirstOrDefault()?.FilePath ?? string.Empty; var file = volume.Chapters.FirstOrDefault()?.Files?.FirstOrDefault()?.FilePath ?? string.Empty;
if (!string.IsNullOrEmpty(file) && _directoryService.FileSystem.File.Exists(file)) if (!string.IsNullOrEmpty(file) && _directoryService.FileSystem.File.Exists(file))
{ {
// This can happen when file is renamed and volume is removed
_logger.LogInformation( _logger.LogInformation(
"[ScannerService] Volume cleanup code was trying to remove a volume with a file still existing on disk. File: {File}", "[ScannerService] Volume cleanup code was trying to remove a volume with a file still existing on disk (usually volume marker removed) File: {File}",
file); file);
} }
@ -640,12 +639,19 @@ public class ProcessSeries : IProcessSeries
chapter.UpdateFrom(info); chapter.UpdateFrom(info);
} }
if (chapter == null) continue; if (chapter == null)
{
continue;
}
// Add files // Add files
var specialTreatment = info.IsSpecialInfo();
AddOrUpdateFileForChapter(chapter, info, forceUpdate); AddOrUpdateFileForChapter(chapter, info, forceUpdate);
// TODO: Investigate using the ChapterBuilder here
chapter.Number = Parser.Parser.MinNumberFromRange(info.Chapters).ToString(CultureInfo.InvariantCulture); chapter.Number = Parser.Parser.MinNumberFromRange(info.Chapters).ToString(CultureInfo.InvariantCulture);
chapter.Range = specialTreatment ? info.Filename : info.Chapters; chapter.MinNumber = Parser.Parser.MinNumberFromRange(info.Chapters);
chapter.MaxNumber = Parser.Parser.MaxNumberFromRange(info.Chapters);
chapter.SortOrder = info.IssueOrder;
chapter.Range = chapter.GetNumberTitle();
} }
@ -655,7 +661,7 @@ public class ProcessSeries : IProcessSeries
{ {
if (existingChapter.Files.Count == 0 || !parsedInfos.HasInfo(existingChapter)) if (existingChapter.Files.Count == 0 || !parsedInfos.HasInfo(existingChapter))
{ {
_logger.LogDebug("[ScannerService] Removed chapter {Chapter} for Volume {VolumeNumber} on {SeriesName}", existingChapter.Range, volume.Name, parsedInfos[0].Series); _logger.LogDebug("[ScannerService] Removed chapter {Chapter} for Volume {VolumeNumber} on {SeriesName}", existingChapter.GetNumberTitle(), volume.Name, parsedInfos[0].Series);
volume.Chapters.Remove(existingChapter); volume.Chapters.Remove(existingChapter);
} }
else else
@ -680,6 +686,7 @@ public class ProcessSeries : IProcessSeries
if (!forceUpdate && !_fileService.HasFileBeenModifiedSince(existingFile.FilePath, existingFile.LastModified) && existingFile.Pages != 0) return; if (!forceUpdate && !_fileService.HasFileBeenModifiedSince(existingFile.FilePath, existingFile.LastModified) && existingFile.Pages != 0) return;
existingFile.Pages = _readingItemService.GetNumberOfPages(info.FullFilePath, info.Format); existingFile.Pages = _readingItemService.GetNumberOfPages(info.FullFilePath, info.Format);
existingFile.Extension = fileInfo.Extension.ToLowerInvariant(); existingFile.Extension = fileInfo.Extension.ToLowerInvariant();
existingFile.FileName = Path.GetFileNameWithoutExtension(existingFile.FilePath);
existingFile.Bytes = fileInfo.Length; existingFile.Bytes = fileInfo.Length;
// We skip updating DB here with last modified time so that metadata refresh can do it // We skip updating DB here with last modified time so that metadata refresh can do it
} }

View file

@ -247,11 +247,17 @@ public class Startup
// v0.7.14 // v0.7.14
await MigrateEmailTemplates.Migrate(directoryService, logger); await MigrateEmailTemplates.Migrate(directoryService, logger);
await MigrateVolumeNumber.Migrate(unitOfWork, dataContext, logger); await MigrateVolumeNumber.Migrate(dataContext, logger);
await MigrateWantToReadImport.Migrate(unitOfWork, directoryService, logger); await MigrateWantToReadImport.Migrate(unitOfWork, dataContext, directoryService, logger);
await MigrateManualHistory.Migrate(dataContext, logger); await MigrateManualHistory.Migrate(dataContext, logger);
await MigrateClearNightlyExternalSeriesRecords.Migrate(dataContext, logger); await MigrateClearNightlyExternalSeriesRecords.Migrate(dataContext, logger);
// v0.8.0
await MigrateVolumeLookupName.Migrate(dataContext, unitOfWork, logger);
await MigrateChapterNumber.Migrate(dataContext, logger);
await MigrateMixedSpecials.Migrate(dataContext, unitOfWork, logger);
await MigrateChapterFields.Migrate(dataContext, unitOfWork, 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);
installVersion.Value = BuildInfo.Version.ToString(); installVersion.Value = BuildInfo.Version.ToString();

View file

@ -9,12 +9,12 @@ Setup guides, FAQ, the more information we have on the [wiki](https://wiki.kavit
### Tools required ### ### Tools required ###
- Visual Studio 2019 or higher (https://www.visualstudio.com/vs/). The community version is free and works fine. [Download it here](https://www.visualstudio.com/downloads/). - Visual Studio 2019 or higher (https://www.visualstudio.com/vs/). The community version is free and works fine. [Download it here](https://www.visualstudio.com/downloads/).
- Rider (optional to Visual Studio) (https://www.jetbrains.com/rider/) - Rider (optional to Visual Studio, preferred editor) (https://www.jetbrains.com/rider/)
- HTML/Javascript editor of choice (VS Code/Sublime Text/Webstorm/Atom/etc) - HTML/Javascript editor of choice (VS Code/Sublime Text/Webstorm/Atom/etc)
- [Git](https://git-scm.com/downloads) - [Git](https://git-scm.com/downloads)
- [NodeJS](https://nodejs.org/en/download/) (Node 18.13.X or higher) - [NodeJS](https://nodejs.org/en/download/) (Node 18.13.X or higher)
- .NET 7.0+ - .NET 8.0+
- dotnet tool install -g --version 6.4.0 Swashbuckle.AspNetCore.Cli - dotnet tool install -g --version 6.5.0 Swashbuckle.AspNetCore.Cli
### Getting started ### ### Getting started ###
@ -24,6 +24,7 @@ Setup guides, FAQ, the more information we have on the [wiki](https://wiki.kavit
- cd Kavita/UI/Web - cd Kavita/UI/Web
- `npm install` - `npm install`
- `npm install -g @angular/cli` - `npm install -g @angular/cli`
- `npm run cache-locale-prime` (only do this once to generate the locale file)
4. Start angular server `ng serve` 4. Start angular server `ng serve`
5. Build the project in Visual Studio/Rider, Setting startup project to `API` 5. Build the project in Visual Studio/Rider, Setting startup project to `API`
6. Debug the project in Visual Studio/Rider 6. Debug the project in Visual Studio/Rider

View file

@ -8,7 +8,7 @@
"minify-langs": "node minify-json.js", "minify-langs": "node minify-json.js",
"cache-locale": "node hash-localization.js", "cache-locale": "node hash-localization.js",
"cache-locale-prime": "node hash-localization-prime.js", "cache-locale-prime": "node hash-localization-prime.js",
"prod": "npm run cache-locale-prime && ng build --configuration production && npm run minify-langs && npm run cache-locale", "prod": "npm run cache-locale-prime && ng build --configuration production && npm run minify-langs && npm run cache-locale",
"explore": "ng build --stats-json && webpack-bundle-analyzer dist/stats.json", "explore": "ng build --stats-json && webpack-bundle-analyzer dist/stats.json",
"lint": "ng lint", "lint": "ng lint",
"e2e": "ng e2e" "e2e": "ng e2e"

View file

@ -1,7 +1,8 @@
import { MangaFile } from './manga-file'; import { MangaFile } from './manga-file';
import { AgeRating } from './metadata/age-rating'; import { AgeRating } from './metadata/age-rating';
export const LooseLeafOrSpecialNumber = 0; export const LooseLeafOrDefaultNumber = -100000;
export const SpecialVolumeNumber = 100000;
/** /**
* Chapter table object. This does not have metadata on it, use ChapterMetadata which is the same Chapter but with those fields. * Chapter table object. This does not have metadata on it, use ChapterMetadata which is the same Chapter but with those fields.
@ -9,7 +10,12 @@ export const LooseLeafOrSpecialNumber = 0;
export interface Chapter { export interface Chapter {
id: number; id: number;
range: string; range: string;
/**
* @deprecated Use minNumber/maxNumber
*/
number: string; number: string;
minNumber: number;
maxNumber: number;
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

@ -8,7 +8,6 @@ import {TranslocoService} from "@ngneat/transloco";
}) })
export class DefaultDatePipe implements PipeTransform { export class DefaultDatePipe implements PipeTransform {
// TODO: Figure out how to translate Never
constructor(private translocoService: TranslocoService) { constructor(private translocoService: TranslocoService) {
} }
transform(value: any, replacementString = 'default-date-pipe.never'): string { transform(value: any, replacementString = 'default-date-pipe.never'): string {

View file

@ -62,7 +62,15 @@
<td> <td>
<ng-container [ngSwitch]="item.scrobbleEventType"> <ng-container [ngSwitch]="item.scrobbleEventType">
<ng-container *ngSwitchCase="ScrobbleEventType.ChapterRead"> <ng-container *ngSwitchCase="ScrobbleEventType.ChapterRead">
{{t('volume-and-chapter-num', {v: item.volumeNumber, n: item.chapterNumber})}} @if(item.volumeNumber === SpecialVolumeNumber) {
{{t('chapter-num', {num: item.volumeNumber})}}
} @else if (item.chapterNumber === LooseLeafOrDefaultNumber) {
{{t('volume-num', {num: item.volumeNumber})}}
} @else if (item.chapterNumber === LooseLeafOrDefaultNumber && item.volumeNumber === SpecialVolumeNumber) {
} @else {
{{t('volume-and-chapter-num', {v: item.volumeNumber, n: item.chapterNumber})}}
}
</ng-container> </ng-container>
<ng-container *ngSwitchCase="ScrobbleEventType.ScoreUpdated"> <ng-container *ngSwitchCase="ScrobbleEventType.ScoreUpdated">
{{t('rating', {r: item.rating})}} {{t('rating', {r: item.rating})}}

View file

@ -16,6 +16,7 @@ import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
import {TranslocoLocaleModule} from "@ngneat/transloco-locale"; import {TranslocoLocaleModule} from "@ngneat/transloco-locale";
import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe"; import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
import {ToastrService} from "ngx-toastr"; import {ToastrService} from "ngx-toastr";
import {LooseLeafOrDefaultNumber, SpecialVolumeNumber} from "../../_models/chapter";
@Component({ @Component({
selector: 'app-user-scrobble-history', selector: 'app-user-scrobble-history',
@ -101,4 +102,6 @@ export class UserScrobbleHistoryComponent implements OnInit {
} }
protected readonly SpecialVolumeNumber = SpecialVolumeNumber;
protected readonly LooseLeafOrDefaultNumber = LooseLeafOrDefaultNumber;
} }

View file

@ -49,7 +49,7 @@ export class ManageLogsComponent implements OnInit, OnDestroy {
this.hubConnection.on('SendLogAsObject', resp => { this.hubConnection.on('SendLogAsObject', resp => {
const payload = resp.arguments[0] as LogMessage; const payload = resp.arguments[0] as LogMessage;
const logMessage = {timestamp: payload.timestamp, level: payload.level, message: payload.message, exception: payload.exception}; const logMessage = {timestamp: payload.timestamp, level: payload.level, message: payload.message, exception: payload.exception};
// TODO: It might be better to just have a queue to show this // NOTE: It might be better to just have a queue to show this
const values = this.logsSource.getValue(); const values = this.logsSource.getValue();
values.push(logMessage); values.push(logMessage);
this.logsSource.next(values); this.logsSource.next(values);
@ -60,7 +60,7 @@ export class ManageLogsComponent implements OnInit, OnDestroy {
} }
ngOnDestroy(): void { ngOnDestroy(): void {
// unsubscrbe from signalr connection // unsubscribe from signalr connection
if (this.hubConnection) { if (this.hubConnection) {
this.hubConnection.stop().catch(err => console.error(err)); this.hubConnection.stop().catch(err => console.error(err));
console.log('Stoping log connection'); console.log('Stoping log connection');

View file

@ -297,6 +297,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) => {
// TODO: Identify how to fix this hack
f.chapter = c.number; f.chapter = c.number;
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 !== LooseLeafOrSpecialNumber; else specialHeader">
{{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}} {{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
</ng-container> </ng-container>
</span> </span>

View file

@ -20,7 +20,7 @@ import { ToastrService } from 'ngx-toastr';
import { Observable, of, map, shareReplay } from 'rxjs'; import { Observable, of, map, shareReplay } from 'rxjs';
import { DownloadService } from 'src/app/shared/_services/download.service'; import { DownloadService } from 'src/app/shared/_services/download.service';
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service'; import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
import { Chapter } from 'src/app/_models/chapter'; import {Chapter, LooseLeafOrDefaultNumber} from 'src/app/_models/chapter';
import { ChapterMetadata } from 'src/app/_models/metadata/chapter-metadata'; import { ChapterMetadata } from 'src/app/_models/metadata/chapter-metadata';
import { Device } from 'src/app/_models/device/device'; import { Device } from 'src/app/_models/device/device';
import { LibraryType } from 'src/app/_models/library/library'; import { LibraryType } from 'src/app/_models/library/library';
@ -74,6 +74,7 @@ export class CardDetailDrawerComponent implements OnInit {
protected readonly Breakpoint = Breakpoint; protected readonly Breakpoint = Breakpoint;
protected readonly LibraryType = LibraryType; protected readonly LibraryType = LibraryType;
protected readonly TabID = TabID; protected readonly TabID = TabID;
protected readonly LooseLeafOrSpecialNumber = LooseLeafOrDefaultNumber;
@Input() parentName = ''; @Input() parentName = '';
@Input() seriesId: number = 0; @Input() seriesId: number = 0;
@ -182,10 +183,10 @@ export class CardDetailDrawerComponent implements OnInit {
} }
formatChapterNumber(chapter: Chapter) { formatChapterNumber(chapter: Chapter) {
if (chapter.number === '0') { if (chapter.minNumber === LooseLeafOrDefaultNumber) {
return '1'; return '1';
} }
return chapter.number; return chapter.minNumber + '';
} }
performAction(action: ActionItem<any>, chapter: Chapter) { performAction(action: ActionItem<any>, chapter: Chapter) {
@ -281,5 +282,4 @@ export class CardDetailDrawerComponent implements OnInit {
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
} }
} }

View file

@ -204,7 +204,7 @@ export class CardItemComponent implements OnInit {
if (volumeTitle === '' || volumeTitle === null || volumeTitle === undefined) { if (volumeTitle === '' || volumeTitle === null || volumeTitle === undefined) {
this.tooltipTitle = (this.title).trim(); this.tooltipTitle = (this.title).trim();
} else { } else {
this.tooltipTitle = (this.utilityService.asChapter(this.entity).volumeTitle + ' ' + this.title).trim(); this.tooltipTitle = (volumeTitle + ' ' + this.title).trim();
} }
} else { } else {
this.tooltipTitle = chapterTitle; this.tooltipTitle = chapterTitle;

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 !== ''">
{{Number !== LooseLeafOrSpecialNumber ? (isChapter && includeVolume ? volumeTitle : '') : ''}} {{Number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
</ng-container> </ng-container>
{{Number !== LooseLeafOrSpecialNumber ? (isChapter ? t('issue-num') + Number : volumeTitle) : t('special')}} {{Number !== LooseLeafOrSpecial ? (isChapter ? t('issue-num') + Number : 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 !== ''">
{{Number !== LooseLeafOrSpecialNumber ? (isChapter && includeVolume ? volumeTitle : '') : ''}} {{Number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
</ng-container> </ng-container>
{{Number !== LooseLeafOrSpecialNumber ? (isChapter ? (t('chapter') + ' ') + Number : volumeTitle) : t('special')}} {{Number !== LooseLeafOrSpecial ? (isChapter ? (t('chapter') + ' ') + Number : volumeTitle) : t('special')}}
</ng-template> </ng-template>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="LibraryType.Book"> <ng-container *ngSwitchCase="LibraryType.Book">

View file

@ -1,11 +1,14 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { UtilityService } from 'src/app/shared/_services/utility.service'; import { UtilityService } from 'src/app/shared/_services/utility.service';
import { Chapter, LooseLeafOrSpecialNumber } from 'src/app/_models/chapter'; import { Chapter, LooseLeafOrDefaultNumber } from 'src/app/_models/chapter';
import { LibraryType } from 'src/app/_models/library/library'; import { LibraryType } from 'src/app/_models/library/library';
import { Volume } from 'src/app/_models/volume'; import { Volume } from 'src/app/_models/volume';
import {CommonModule, NgSwitch} from "@angular/common"; import {CommonModule, NgSwitch} from "@angular/common";
import {TranslocoModule} from "@ngneat/transloco"; import {TranslocoModule} from "@ngneat/transloco";
/**
* This is primarily used for list item
*/
@Component({ @Component({
selector: 'app-entity-title', selector: 'app-entity-title',
standalone: true, standalone: true,
@ -20,7 +23,9 @@ import {TranslocoModule} from "@ngneat/transloco";
}) })
export class EntityTitleComponent implements OnInit { export class EntityTitleComponent implements OnInit {
protected readonly LooseLeafOrSpecialNumber = LooseLeafOrSpecialNumber; protected readonly LooseLeafOrSpecialNumber = LooseLeafOrDefaultNumber;
protected readonly LooseLeafOrSpecial = LooseLeafOrDefaultNumber + "";
protected readonly LibraryType = LibraryType;
/** /**
* Library type for which the entity belongs * Library type for which the entity belongs
@ -42,19 +47,18 @@ export class EntityTitleComponent implements OnInit {
volumeTitle: string = ''; volumeTitle: string = '';
get Number() { get Number() {
if (this.utilityService.isVolume(this.entity)) return (this.entity as Volume).minNumber; if (this.isChapter) return (this.entity as Chapter).range;
return (this.entity as Chapter).number; return (this.entity as Volume).name;
} }
get LibraryType() {
return LibraryType;
}
constructor(private utilityService: UtilityService, private readonly cdRef: ChangeDetectorRef) {} constructor(private utilityService: UtilityService, private readonly cdRef: ChangeDetectorRef) {}
ngOnInit(): void { ngOnInit(): void {
this.isChapter = this.utilityService.isChapter(this.entity); this.isChapter = this.utilityService.isChapter(this.entity);
if (this.isChapter) { if (this.isChapter) {
const c = (this.entity as Chapter); const c = (this.entity as Chapter);
this.volumeTitle = c.volumeTitle || ''; this.volumeTitle = c.volumeTitle || '';

View file

@ -308,7 +308,7 @@
<ng-container *ngIf="nextExpectedChapter"> <ng-container *ngIf="nextExpectedChapter">
<ng-container [ngSwitch]="tabId"> <ng-container [ngSwitch]="tabId">
<ng-container *ngSwitchCase="TabID.Volumes"> <ng-container *ngSwitchCase="TabID.Volumes">
<app-next-expected-card *ngIf="nextExpectedChapter.volumeNumber > 0 && nextExpectedChapter.chapterNumber === LooseLeafOrSpecialNumber" <app-next-expected-card *ngIf="nextExpectedChapter.volumeNumber !== SpecialVolumeNumber && nextExpectedChapter.chapterNumber === LooseLeafOrSpecialNumber"
class="col-auto mt-2 mb-2" [entity]="nextExpectedChapter" class="col-auto mt-2 mb-2" [entity]="nextExpectedChapter"
[imageUrl]="imageService.getSeriesCoverImage(series.id)"></app-next-expected-card> [imageUrl]="imageService.getSeriesCoverImage(series.id)"></app-next-expected-card>
</ng-container> </ng-container>

View file

@ -55,7 +55,7 @@ import {
import {TagBadgeCursor} from 'src/app/shared/tag-badge/tag-badge.component'; import {TagBadgeCursor} from 'src/app/shared/tag-badge/tag-badge.component';
import {DownloadEvent, DownloadService} from 'src/app/shared/_services/download.service'; import {DownloadEvent, DownloadService} from 'src/app/shared/_services/download.service';
import {KEY_CODES, UtilityService} from 'src/app/shared/_services/utility.service'; import {KEY_CODES, UtilityService} from 'src/app/shared/_services/utility.service';
import {Chapter} from 'src/app/_models/chapter'; import {Chapter, SpecialVolumeNumber} from 'src/app/_models/chapter';
import {Device} from 'src/app/_models/device/device'; import {Device} from 'src/app/_models/device/device';
import {ScanSeriesEvent} from 'src/app/_models/events/scan-series-event'; import {ScanSeriesEvent} from 'src/app/_models/events/scan-series-event';
import {SeriesRemovedEvent} from 'src/app/_models/events/series-removed-event'; import {SeriesRemovedEvent} from 'src/app/_models/events/series-removed-event';
@ -67,7 +67,7 @@ import {RelationKind} from 'src/app/_models/series-detail/relation-kind';
import {SeriesMetadata} from 'src/app/_models/metadata/series-metadata'; import {SeriesMetadata} from 'src/app/_models/metadata/series-metadata';
import {User} from 'src/app/_models/user'; import {User} from 'src/app/_models/user';
import {Volume} from 'src/app/_models/volume'; import {Volume} from 'src/app/_models/volume';
import {LooseLeafOrSpecialNumber} from 'src/app/_models/chapter'; import {LooseLeafOrDefaultNumber} from 'src/app/_models/chapter';
import {AccountService} from 'src/app/_services/account.service'; import {AccountService} from 'src/app/_services/account.service';
import {Action, ActionFactoryService, ActionItem} from 'src/app/_services/action-factory.service'; import {Action, ActionFactoryService, ActionItem} from 'src/app/_services/action-factory.service';
import {ActionService} from 'src/app/_services/action.service'; import {ActionService} from 'src/app/_services/action.service';
@ -184,7 +184,8 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
protected readonly PageLayoutMode = PageLayoutMode; protected readonly PageLayoutMode = PageLayoutMode;
protected readonly TabID = TabID; protected readonly TabID = TabID;
protected readonly TagBadgeCursor = TagBadgeCursor; protected readonly TagBadgeCursor = TagBadgeCursor;
protected readonly LooseLeafOrSpecialNumber = LooseLeafOrSpecialNumber; protected readonly LooseLeafOrSpecialNumber = LooseLeafOrDefaultNumber;
protected readonly SpecialVolumeNumber = SpecialVolumeNumber;
@ViewChild('scrollingBlock') scrollingBlock: ElementRef<HTMLDivElement> | undefined; @ViewChild('scrollingBlock') scrollingBlock: ElementRef<HTMLDivElement> | undefined;
@ViewChild('companionBar') companionBar: ElementRef<HTMLDivElement> | undefined; @ViewChild('companionBar') companionBar: ElementRef<HTMLDivElement> | undefined;
@ -241,7 +242,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.maxNumber}_${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) => {
@ -371,13 +372,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; // TODO: Refactor this to use DisplayTitle (or Range) and Localize it
} }
if (this.currentlyReadingChapter.number === "0") { if (this.currentlyReadingChapter.minNumber === LooseLeafOrDefaultNumber) {
return 'Vol ' + vol[0].minNumber; return 'Vol ' + vol[0].minNumber;
} }
return 'Vol ' + vol[0].minNumber + ' Ch ' + this.currentlyReadingChapter.number; return 'Vol ' + vol[0].minNumber + ' Ch ' + this.currentlyReadingChapter.minNumber;
} }
return this.currentlyReadingChapter.title; return this.currentlyReadingChapter.title;

View file

@ -119,7 +119,7 @@ export class DownloadService {
case 'volume': case 'volume':
return (downloadEntity as Volume).minNumber + ''; 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 a.minNumber - b.minNumber;
} }
mangaFormatToText(format: MangaFormat): string { mangaFormatToText(format: MangaFormat): string {

View file

@ -6,5 +6,5 @@ export interface ReadHistoryEvent {
libraryId: number; libraryId: number;
readDate: string; readDate: string;
chapterId: number; chapterId: number;
chapterNumber: string; chapterNumber: number;
} }

View file

@ -42,6 +42,8 @@
"is-processed-header": "Is Processed", "is-processed-header": "Is Processed",
"no-data": "No Data", "no-data": "No Data",
"volume-and-chapter-num": "Volume {{v}} Chapter {{n}}", "volume-and-chapter-num": "Volume {{v}} Chapter {{n}}",
"volume-num": "Volume {{num}}",
"chapter-num": "Chapter {{num}}",
"rating": "Rating {{r}}", "rating": "Rating {{r}}",
"not-applicable": "Not Applicable", "not-applicable": "Not Applicable",
"processed": "Processed", "processed": "Processed",

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.14.2" "version": "0.7.14.3"
}, },
"servers": [ "servers": [
{ {
@ -13718,13 +13718,29 @@
}, },
"range": { "range": {
"type": "string", "type": "string",
"description": "Range of numbers. Chapter 2-4 -> \"2-4\". Chapter 2 -> \"2\".", "description": "Range of numbers. Chapter 2-4 -> \"2-4\". Chapter 2 -> \"2\". If the chapter is a special, will return the Special Name",
"nullable": true "nullable": true
}, },
"number": { "number": {
"type": "string", "type": "string",
"description": "Smallest number of the Range. Can be a partial like Chapter 4.5", "description": "Smallest number of the Range. Can be a partial like Chapter 4.5",
"nullable": true "nullable": true,
"deprecated": true
},
"minNumber": {
"type": "number",
"description": "Minimum Chapter Number.",
"format": "float"
},
"maxNumber": {
"type": "number",
"description": "Maximum Chapter Number",
"format": "float"
},
"sortOrder": {
"type": "number",
"description": "The sorting order of the Chapter. Inherits from MinNumber, but can be overridden.",
"format": "float"
}, },
"files": { "files": {
"type": "array", "type": "array",
@ -13926,13 +13942,26 @@
}, },
"range": { "range": {
"type": "string", "type": "string",
"description": "Range of chapters. Chapter 2-4 -> \"2-4\". Chapter 2 -> \"2\".", "description": "Range of chapters. Chapter 2-4 -> \"2-4\". Chapter 2 -> \"2\". If special, will be special name.",
"nullable": true "nullable": true
}, },
"number": { "number": {
"type": "string", "type": "string",
"description": "Smallest number of the Range.", "description": "Smallest number of the Range.",
"nullable": true "nullable": true,
"deprecated": true
},
"minNumber": {
"type": "number",
"format": "float"
},
"maxNumber": {
"type": "number",
"format": "float"
},
"sortOrder": {
"type": "number",
"format": "float"
}, },
"pages": { "pages": {
"type": "integer", "type": "integer",
@ -16163,6 +16192,11 @@
"type": "integer", "type": "integer",
"format": "int32" "format": "int32"
}, },
"fileName": {
"type": "string",
"description": "The filename without extension",
"nullable": true
},
"filePath": { "filePath": {
"type": "string", "type": "string",
"description": "Absolute path to the archive file", "description": "Absolute path to the archive file",
@ -16691,8 +16725,8 @@
"format": "int32" "format": "int32"
}, },
"chapterNumber": { "chapterNumber": {
"type": "string", "type": "number",
"nullable": true "format": "float"
} }
}, },
"additionalProperties": false, "additionalProperties": false,
@ -20376,6 +20410,11 @@
"description": "A String representation of the volume number. Allows for floats. Can also include a range (1-2).", "description": "A String representation of the volume number. Allows for floats. Can also include a range (1-2).",
"nullable": true "nullable": true
}, },
"lookupName": {
"type": "string",
"description": "This is just the original Parsed volume number for lookups",
"nullable": true
},
"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",
@ -20472,9 +20511,9 @@
"nullable": true "nullable": true
}, },
"number": { "number": {
"type": "number", "type": "integer",
"description": "This will map to MinNumber. Number was removed in v0.7.13.8/v0.7.14", "description": "This will map to MinNumber. Number was removed in v0.7.13.8/v0.7.14",
"format": "float", "format": "int32",
"deprecated": true "deprecated": true
}, },
"pages": { "pages": {