Foundational Rework (#2745)
This commit is contained in:
parent
42cd6e9b3a
commit
4fa21fe1ca
92 changed files with 13330 additions and 650 deletions
|
@ -4,15 +4,16 @@ using Xunit;
|
|||
|
||||
namespace API.Tests.Comparers;
|
||||
|
||||
public class ChapterSortComparerTest
|
||||
public class ChapterSortComparerDefaultLastTest
|
||||
{
|
||||
[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[] {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)
|
||||
{
|
||||
Assert.Equal(expected, input.OrderBy(f => f, new ChapterSortComparer()).ToArray());
|
||||
Assert.Equal(expected, input.OrderBy(f => f, new ChapterSortComparerDefaultLast()).ToArray());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ using Xunit;
|
|||
|
||||
namespace API.Tests.Comparers;
|
||||
|
||||
public class ChapterSortComparerZeroFirstTests
|
||||
public class ChapterSortComparerDefaultFirstTests
|
||||
{
|
||||
[Theory]
|
||||
[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})]
|
||||
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]
|
||||
[InlineData(new[] {1.0, 0.5, 0.3}, new[] {0.3, 0.5, 1.0})]
|
||||
public void ChapterSortComparerZeroFirstTest_Doubles(double[] input, double[] expected)
|
||||
[InlineData(new [] {1.0f, 0.5f, 0.3f}, new [] {0.3f, 0.5f, 1.0f})]
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,11 +7,11 @@ namespace API.Tests.Comparers;
|
|||
public class SortComparerZeroLastTests
|
||||
{
|
||||
[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[] {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)
|
||||
{
|
||||
Assert.Equal(expected, input.OrderBy(f => f, SortComparerZeroLast.Default).ToArray());
|
||||
Assert.Equal(expected, input.OrderBy(f => f, ChapterSortComparerDefaultLast.Default).ToArray());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,22 +17,23 @@ public class SeriesExtensionsTests
|
|||
{
|
||||
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)
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
|
||||
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
|
||||
.WithCoverImage("Special 1")
|
||||
.WithIsSpecial(true)
|
||||
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1)
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
|
||||
.WithCoverImage("Special 2")
|
||||
.WithIsSpecial(true)
|
||||
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 2)
|
||||
.Build())
|
||||
.Build())
|
||||
.Build();
|
||||
|
||||
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());
|
||||
|
@ -67,12 +68,36 @@ public class SeriesExtensionsTests
|
|||
|
||||
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());
|
||||
}
|
||||
|
||||
[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]
|
||||
public void GetCoverImage_JustVolumes()
|
||||
{
|
||||
|
@ -109,7 +134,7 @@ public class SeriesExtensionsTests
|
|||
|
||||
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());
|
||||
|
@ -135,7 +160,7 @@ public class SeriesExtensionsTests
|
|||
|
||||
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());
|
||||
|
@ -156,16 +181,19 @@ public class SeriesExtensionsTests
|
|||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Chapter 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)
|
||||
.WithCoverImage("Special 1")
|
||||
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1)
|
||||
.Build())
|
||||
.Build())
|
||||
.Build())
|
||||
.Build();
|
||||
|
||||
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());
|
||||
|
@ -186,9 +214,12 @@ public class SeriesExtensionsTests
|
|||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Chapter 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)
|
||||
.WithCoverImage("Special 3")
|
||||
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1)
|
||||
.Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
|
@ -202,7 +233,7 @@ public class SeriesExtensionsTests
|
|||
|
||||
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());
|
||||
|
@ -223,9 +254,12 @@ public class SeriesExtensionsTests
|
|||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Chapter 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)
|
||||
.WithCoverImage("Special 1")
|
||||
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1)
|
||||
.Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
|
@ -239,7 +273,7 @@ public class SeriesExtensionsTests
|
|||
|
||||
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());
|
||||
|
@ -260,9 +294,12 @@ public class SeriesExtensionsTests
|
|||
.WithIsSpecial(false)
|
||||
.WithCoverImage("Chapter 1425")
|
||||
.Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
|
||||
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
|
||||
.WithIsSpecial(true)
|
||||
.WithCoverImage("Special 1")
|
||||
.WithCoverImage("Special 3")
|
||||
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1)
|
||||
.Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
|
@ -283,7 +320,7 @@ public class SeriesExtensionsTests
|
|||
|
||||
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());
|
||||
|
@ -316,7 +353,7 @@ public class SeriesExtensionsTests
|
|||
|
||||
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());
|
||||
|
|
|
@ -23,10 +23,41 @@ public class VolumeListExtensionsTests
|
|||
.Build(),
|
||||
new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.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(),
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -41,7 +72,12 @@ public class VolumeListExtensionsTests
|
|||
.Build(),
|
||||
new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.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(),
|
||||
};
|
||||
|
||||
|
@ -59,7 +95,12 @@ public class VolumeListExtensionsTests
|
|||
.Build(),
|
||||
new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.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(),
|
||||
};
|
||||
|
||||
|
@ -77,7 +118,12 @@ public class VolumeListExtensionsTests
|
|||
.Build(),
|
||||
new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.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(),
|
||||
};
|
||||
|
||||
|
@ -95,7 +141,12 @@ public class VolumeListExtensionsTests
|
|||
.Build(),
|
||||
new VolumeBuilder("1")
|
||||
.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(),
|
||||
};
|
||||
|
||||
|
|
|
@ -198,7 +198,7 @@ public class DefaultParserTests
|
|||
filepath = @"E:\Manga\Summer Time Rendering\Specials\Record 014 (between chapter 083 and ch084) SP11.cbr";
|
||||
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,
|
||||
FullFilePath = filepath, IsSpecial = true
|
||||
});
|
||||
|
@ -414,7 +414,7 @@ public class DefaultParserTests
|
|||
filepath = @"E:/Manga/Foo 50/Specials/Foo 50 SP01.cbz";
|
||||
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,
|
||||
FullFilePath = filepath
|
||||
};
|
||||
|
@ -449,7 +449,7 @@ public class DefaultParserTests
|
|||
var filepath = @"E:/Comics/Teen Titans/Teen Titans v1 Annual 01 (1967) SP01.cbr";
|
||||
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,
|
||||
FullFilePath = filepath
|
||||
});
|
||||
|
|
|
@ -294,6 +294,7 @@ public class MangaParserTests
|
|||
[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 c01-c04", "1-4")]
|
||||
[InlineData("Adabana c00-02", "0-2")]
|
||||
public void ParseChaptersTest(string filename, string expected)
|
||||
{
|
||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename));
|
||||
|
|
|
@ -45,6 +45,18 @@ public class ParserTests
|
|||
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]
|
||||
[InlineData("0001", "1")]
|
||||
[InlineData("1", "1")]
|
||||
|
@ -155,6 +167,7 @@ public class ParserTests
|
|||
[InlineData("3.5", 3.5)]
|
||||
[InlineData("3.5-4.0", 3.5)]
|
||||
[InlineData("asdfasdf", 0.0)]
|
||||
[InlineData("-10", -10.0)]
|
||||
public void MinimumNumberFromRangeTest(string input, float expected)
|
||||
{
|
||||
Assert.Equal(expected, MinNumberFromRange(input));
|
||||
|
@ -171,6 +184,7 @@ public class ParserTests
|
|||
[InlineData("3.5", 3.5)]
|
||||
[InlineData("3.5-4.0", 4.0)]
|
||||
[InlineData("asdfasdf", 0.0)]
|
||||
[InlineData("-10", -10.0)]
|
||||
public void MaximumNumberFromRangeTest(string input, float expected)
|
||||
{
|
||||
Assert.Equal(expected, MaxNumberFromRange(input));
|
||||
|
|
|
@ -395,7 +395,6 @@ public class CleanupServiceTests : AbstractDbTest
|
|||
var series = new SeriesBuilder("Test")
|
||||
.WithFormat(MangaFormat.Epub)
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(c)
|
||||
.Build())
|
||||
.Build();
|
||||
|
|
|
@ -136,7 +136,6 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
|
||||
.WithPages(1)
|
||||
.Build())
|
||||
|
@ -166,7 +165,6 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
|
||||
.WithPages(1)
|
||||
.Build())
|
||||
|
@ -205,7 +203,6 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
|
||||
.WithPages(1)
|
||||
.Build())
|
||||
|
@ -260,7 +257,6 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
|
||||
.WithPages(1)
|
||||
.Build())
|
||||
|
@ -299,7 +295,6 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
|
||||
.WithPages(1)
|
||||
.Build())
|
||||
|
@ -347,19 +342,16 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithMinNumber(2)
|
||||
.WithChapter(new ChapterBuilder("21").Build())
|
||||
.WithChapter(new ChapterBuilder("22").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("3")
|
||||
.WithMinNumber(3)
|
||||
.WithChapter(new ChapterBuilder("31").Build())
|
||||
.WithChapter(new ChapterBuilder("32").Build())
|
||||
.Build())
|
||||
|
@ -379,6 +371,7 @@ public class ReaderServiceTests
|
|||
|
||||
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 1, 1);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||
Assert.NotNull(actualChapter);
|
||||
Assert.Equal("2", actualChapter.Range);
|
||||
}
|
||||
|
||||
|
@ -390,12 +383,10 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1-2")
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("3-4")
|
||||
.WithMinNumber(2)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.Build())
|
||||
.Build();
|
||||
|
@ -412,6 +403,7 @@ public class ReaderServiceTests
|
|||
|
||||
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 1, 1);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||
Assert.NotNull(actualChapter);
|
||||
Assert.Equal("3-4", actualChapter.Volume.Name);
|
||||
Assert.Equal("1", actualChapter.Range);
|
||||
}
|
||||
|
@ -456,6 +448,7 @@ public class ReaderServiceTests
|
|||
|
||||
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 2, 1);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||
Assert.NotNull(actualChapter);
|
||||
Assert.Equal("31", actualChapter.Range);
|
||||
}
|
||||
|
||||
|
@ -466,19 +459,16 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithMinNumber(2)
|
||||
.WithChapter(new ChapterBuilder("21").Build())
|
||||
.WithChapter(new ChapterBuilder("22").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("3")
|
||||
.WithMinNumber(3)
|
||||
.WithChapter(new ChapterBuilder("31").Build())
|
||||
.WithChapter(new ChapterBuilder("32").Build())
|
||||
.Build())
|
||||
|
@ -497,6 +487,7 @@ public class ReaderServiceTests
|
|||
|
||||
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||
Assert.NotNull(actualChapter);
|
||||
Assert.Equal("21", actualChapter.Range);
|
||||
}
|
||||
|
||||
|
@ -507,19 +498,16 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("1.5")
|
||||
.WithMinNumber(2)
|
||||
.WithChapter(new ChapterBuilder("21").Build())
|
||||
.WithChapter(new ChapterBuilder("22").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("3")
|
||||
.WithMinNumber(3)
|
||||
.WithChapter(new ChapterBuilder("31").Build())
|
||||
.WithChapter(new ChapterBuilder("32").Build())
|
||||
.Build())
|
||||
|
@ -539,6 +527,7 @@ public class ReaderServiceTests
|
|||
|
||||
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||
Assert.NotNull(actualChapter);
|
||||
Assert.Equal("21", actualChapter.Range);
|
||||
}
|
||||
|
||||
|
@ -549,15 +538,13 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.WithChapter(new ChapterBuilder("21").Build())
|
||||
.WithChapter(new ChapterBuilder("22").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("21").Build())
|
||||
.WithChapter(new ChapterBuilder("22").Build())
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
.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);
|
||||
Assert.NotEqual(-1, nextChapter);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||
Assert.Equal("1", actualChapter.Range);
|
||||
Assert.NotNull(actualChapter);
|
||||
Assert.Equal("21", actualChapter.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -584,18 +572,15 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("66").Build())
|
||||
.WithChapter(new ChapterBuilder("67").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithMinNumber(2)
|
||||
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build())
|
||||
.Build())
|
||||
.Build();
|
||||
|
@ -616,6 +601,7 @@ public class ReaderServiceTests
|
|||
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 3, 1);
|
||||
Assert.NotEqual(-1, nextChapter);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||
Assert.NotNull(actualChapter);
|
||||
Assert.Equal(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, actualChapter.Range);
|
||||
}
|
||||
|
||||
|
@ -626,15 +612,13 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build())
|
||||
.WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).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).Build())
|
||||
.WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).Build())
|
||||
.Build())
|
||||
.Build();
|
||||
series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
|
||||
|
@ -658,7 +642,6 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
|
@ -684,7 +667,6 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").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)
|
||||
// [Fact]
|
||||
// public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromLastChapter_NoSpecials_FirstIsVolume()
|
||||
// {
|
||||
// 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);
|
||||
// }
|
||||
[Fact]
|
||||
public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromLastChapter_NoSpecials_FirstIsVolume()
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
// This is commented out because, while valid, I can't solve how to make this pass
|
||||
// [Fact]
|
||||
// public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromLastChapter_WithSpecials()
|
||||
// {
|
||||
// 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())
|
||||
// .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithIsSpecial(true).Build())
|
||||
// .Build())
|
||||
//
|
||||
// .WithVolume(new VolumeBuilder("1")
|
||||
// .WithMinNumber(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, 4, 1);
|
||||
// Assert.Equal(-1, nextChapter);
|
||||
// }
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("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, 1, 2, 1);
|
||||
Assert.Equal(-1, nextChapter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetNextChapterIdAsync_ShouldFindNoNextChapterFromLastChapter_WithSpecials()
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.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")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build())
|
||||
.WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).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)
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder("B.cbz")
|
||||
.WithIsSpecial(true)
|
||||
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 2)
|
||||
.Build())
|
||||
.Build())
|
||||
.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);
|
||||
Assert.NotEqual(-1, nextChapter);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||
Assert.NotNull(actualChapter);
|
||||
Assert.Equal("A.cbz", actualChapter.Range);
|
||||
}
|
||||
|
||||
|
@ -812,10 +800,16 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("1").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();
|
||||
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);
|
||||
Assert.NotEqual(-1, nextChapter);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||
Assert.NotNull(actualChapter);
|
||||
Assert.Equal("A.cbz", actualChapter.Range);
|
||||
}
|
||||
|
||||
|
@ -843,15 +838,21 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).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();
|
||||
series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
|
||||
|
||||
|
@ -864,7 +865,7 @@ public class ReaderServiceTests
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -876,14 +877,18 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build())
|
||||
.WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).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)
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder("B.cbz")
|
||||
.WithIsSpecial(true)
|
||||
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 2)
|
||||
.Build())
|
||||
.Build())
|
||||
.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);
|
||||
Assert.NotEqual(-1, nextChapter);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||
Assert.NotNull(actualChapter);
|
||||
Assert.Equal("B.cbz", actualChapter.Range);
|
||||
}
|
||||
|
||||
|
@ -911,12 +917,10 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("12").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithMinNumber(2)
|
||||
.WithChapter(new ChapterBuilder("12").Build())
|
||||
.Build())
|
||||
.Build();
|
||||
|
@ -952,19 +956,16 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithMinNumber(2)
|
||||
.WithChapter(new ChapterBuilder("21").Build())
|
||||
.WithChapter(new ChapterBuilder("22").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("3")
|
||||
.WithMinNumber(3)
|
||||
.WithChapter(new ChapterBuilder("31").Build())
|
||||
.WithChapter(new ChapterBuilder("32").Build())
|
||||
.Build())
|
||||
|
@ -984,6 +985,7 @@ public class ReaderServiceTests
|
|||
|
||||
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 1, 2, 1);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
|
||||
Assert.NotNull(actualChapter);
|
||||
Assert.Equal("1", actualChapter.Range);
|
||||
}
|
||||
|
||||
|
@ -995,19 +997,16 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("1.5")
|
||||
.WithMinNumber(2)
|
||||
.WithChapter(new ChapterBuilder("21").Build())
|
||||
.WithChapter(new ChapterBuilder("22").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("3")
|
||||
.WithMinNumber(3)
|
||||
.WithChapter(new ChapterBuilder("31").Build())
|
||||
.WithChapter(new ChapterBuilder("32").Build())
|
||||
.Build())
|
||||
|
@ -1025,6 +1024,7 @@ public class ReaderServiceTests
|
|||
|
||||
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 3, 5, 1);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
|
||||
Assert.NotNull(actualChapter);
|
||||
Assert.Equal("22", actualChapter.Range);
|
||||
}
|
||||
|
||||
|
@ -1038,7 +1038,14 @@ public class ReaderServiceTests
|
|||
.WithChapter(new ChapterBuilder("40").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("50").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())
|
||||
|
||||
.WithVolume(new VolumeBuilder("1997")
|
||||
|
@ -1065,7 +1072,7 @@ public class ReaderServiceTests
|
|||
|
||||
|
||||
// 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);
|
||||
Assert.NotNull(actualChapter);
|
||||
|
@ -1109,6 +1116,7 @@ public class ReaderServiceTests
|
|||
|
||||
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 3, 1);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
|
||||
Assert.NotNull(actualChapter);
|
||||
Assert.Equal("2", actualChapter.Range);
|
||||
}
|
||||
|
||||
|
@ -1119,15 +1127,13 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build())
|
||||
.WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).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).Build())
|
||||
.WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 2).Build())
|
||||
.Build())
|
||||
.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);
|
||||
Assert.Equal(2, prevChapter);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
|
||||
Assert.NotNull(actualChapter);
|
||||
Assert.Equal("2", actualChapter.Range);
|
||||
}
|
||||
|
||||
|
@ -1157,7 +1164,6 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
|
@ -1187,7 +1193,6 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).Build())
|
||||
.Build())
|
||||
.Build();
|
||||
|
@ -1216,13 +1221,11 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.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();
|
||||
|
@ -1237,10 +1240,7 @@ public class ReaderServiceTests
|
|||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
|
||||
|
||||
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
|
||||
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 3, 1);
|
||||
Assert.Equal(-1, prevChapter);
|
||||
}
|
||||
|
||||
|
@ -1251,22 +1251,19 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("5").Build())
|
||||
.WithChapter(new ChapterBuilder("6").Build())
|
||||
.WithChapter(new ChapterBuilder("7").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").WithIsSpecial(true).Build())
|
||||
.WithChapter(new ChapterBuilder("2").WithIsSpecial(true).Build())
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithMinNumber(2)
|
||||
.WithChapter(new ChapterBuilder("3").WithIsSpecial(true).Build())
|
||||
.WithChapter(new ChapterBuilder("4").WithIsSpecial(true).Build())
|
||||
.WithChapter(new ChapterBuilder("3").Build())
|
||||
.WithChapter(new ChapterBuilder("4").Build())
|
||||
.Build())
|
||||
.Build();
|
||||
series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
|
||||
|
@ -1299,7 +1296,6 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
|
@ -1329,14 +1325,18 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("A.cbz").WithIsSpecial(true).Build())
|
||||
.WithChapter(new ChapterBuilder("B.cbz").WithIsSpecial(true).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)
|
||||
.Build())
|
||||
.WithChapter(new ChapterBuilder("B.cbz")
|
||||
.WithIsSpecial(true)
|
||||
.WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 2)
|
||||
.Build())
|
||||
.Build())
|
||||
.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);
|
||||
Assert.NotEqual(-1, prevChapter);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
|
||||
Assert.NotNull(actualChapter);
|
||||
Assert.Equal("A.cbz", actualChapter.Range);
|
||||
}
|
||||
|
||||
|
@ -1367,12 +1368,10 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.WithMinNumber(0)
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.WithChapter(new ChapterBuilder("2").Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("21").Build())
|
||||
.WithChapter(new ChapterBuilder("22").Build())
|
||||
.Build())
|
||||
|
@ -1389,12 +1388,10 @@ public class ReaderServiceTests
|
|||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
|
||||
|
||||
var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 1, 1, 1);
|
||||
Assert.NotEqual(-1, prevChapter);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
|
||||
Assert.NotNull(actualChapter);
|
||||
Assert.Equal("22", actualChapter.Range);
|
||||
}
|
||||
|
||||
|
@ -1405,12 +1402,10 @@ public class ReaderServiceTests
|
|||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithMinNumber(1)
|
||||
.WithChapter(new ChapterBuilder("12").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithMinNumber(2)
|
||||
.WithChapter(new ChapterBuilder("12").Build())
|
||||
.Build())
|
||||
.Build();
|
||||
|
@ -1630,7 +1625,12 @@ public class ReaderServiceTests
|
|||
.WithChapter(new ChapterBuilder("46").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("47").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())
|
||||
// One file volume
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
|
@ -1697,7 +1697,9 @@ public class ReaderServiceTests
|
|||
.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("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();
|
||||
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)
|
||||
.WithChapter(new ChapterBuilder("100").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())
|
||||
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
|
@ -2031,7 +2035,9 @@ public class ReaderServiceTests
|
|||
.WithChapter(new ChapterBuilder("1").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("2").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();
|
||||
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("52").WithPages(1).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();
|
||||
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("2").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();
|
||||
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.5").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();
|
||||
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("49").WithPages(49).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())
|
||||
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
|
@ -2550,14 +2564,14 @@ public class ReaderServiceTests
|
|||
public async Task MarkSeriesAsReadTest()
|
||||
{
|
||||
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")
|
||||
|
||||
.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("1").WithPages(2).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("1").WithPages(2).Build())
|
||||
.Build())
|
||||
|
@ -2669,7 +2683,9 @@ public class ReaderServiceTests
|
|||
.WithChapter(new ChapterBuilder("10").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("20").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())
|
||||
.WithVolume(new VolumeBuilder("1997")
|
||||
.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("20").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())
|
||||
.WithVolume(new VolumeBuilder("1997")
|
||||
.WithChapter(new ChapterBuilder("1").WithPages(1).Build())
|
||||
|
|
|
@ -108,9 +108,9 @@ public class SeriesServiceTests : AbstractDbTest
|
|||
.WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build())
|
||||
.WithSeries(new SeriesBuilder("Test")
|
||||
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.WithChapter(new ChapterBuilder("Omake").WithIsSpecial(true).WithTitle("Omake").WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("Something SP02").WithIsSpecial(true).WithTitle("Something").WithPages(1).Build())
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume)
|
||||
.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).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 2).WithTitle("Something").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithChapter(new ChapterBuilder("21").WithPages(1).Build())
|
||||
|
@ -280,11 +280,13 @@ public class SeriesServiceTests : AbstractDbTest
|
|||
.WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build())
|
||||
.WithSeries(new SeriesBuilder("Test")
|
||||
|
||||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.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())
|
||||
.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).WithSortOrder(API.Services.Tasks.Scanner.Parser.Parser.SpecialVolumeNumber + 1).WithPages(1).Build())
|
||||
.Build())
|
||||
.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());
|
||||
|
@ -779,7 +781,9 @@ public class SeriesServiceTests : AbstractDbTest
|
|||
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
|
||||
.WithChapter(new ChapterBuilder("95").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())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithChapter(new ChapterBuilder("1").WithPages(1).WithFile(file).Build())
|
||||
|
@ -829,6 +833,7 @@ public class SeriesServiceTests : AbstractDbTest
|
|||
|
||||
var firstChapter = SeriesService.GetFirstChapterForMetadata(series);
|
||||
Assert.NotNull(firstChapter);
|
||||
Assert.NotNull(firstChapter);
|
||||
Assert.Same("1", firstChapter.Range);
|
||||
}
|
||||
|
||||
|
@ -838,7 +843,8 @@ public class SeriesServiceTests : AbstractDbTest
|
|||
var series = CreateSeriesMock();
|
||||
|
||||
var firstChapter = SeriesService.GetFirstChapterForMetadata(series);
|
||||
Assert.Same("1", firstChapter.Range);
|
||||
Assert.NotNull(firstChapter);
|
||||
Assert.Equal(1, firstChapter.MinNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -849,7 +855,7 @@ public class SeriesServiceTests : AbstractDbTest
|
|||
{
|
||||
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("1.1").WithFiles(files).WithPages(1).Build(),
|
||||
|
@ -857,7 +863,8 @@ public class SeriesServiceTests : AbstractDbTest
|
|||
};
|
||||
|
||||
var firstChapter = SeriesService.GetFirstChapterForMetadata(series);
|
||||
Assert.Same("1.1", firstChapter.Range);
|
||||
Assert.NotNull(firstChapter);
|
||||
Assert.True(firstChapter.MinNumber.Is(1.1f));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -882,7 +889,8 @@ public class SeriesServiceTests : AbstractDbTest
|
|||
series.Library = new LibraryBuilder("Test LIb", LibraryType.Book).Build();
|
||||
|
||||
var firstChapter = SeriesService.GetFirstChapterForMetadata(series);
|
||||
Assert.Same("1", firstChapter.Range);
|
||||
Assert.NotNull(firstChapter);
|
||||
Assert.Equal(1, firstChapter.MinNumber);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -919,6 +927,7 @@ public class SeriesServiceTests : AbstractDbTest
|
|||
addRelationDto.Adaptations.Add(2);
|
||||
addRelationDto.Sequels.Add(3);
|
||||
await _seriesService.UpdateRelatedSeries(addRelationDto);
|
||||
Assert.NotNull(series1);
|
||||
Assert.Equal(2, series1.Relations.Single(s => s.TargetSeriesId == 2).TargetSeriesId);
|
||||
Assert.Equal(3, series1.Relations.Single(s => s.TargetSeriesId == 3).TargetSeriesId);
|
||||
}
|
||||
|
@ -1291,7 +1300,7 @@ public class SeriesServiceTests : AbstractDbTest
|
|||
.Build());
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
|
@ -1323,7 +1332,7 @@ public class SeriesServiceTests : AbstractDbTest
|
|||
.Build());
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
|
@ -1355,7 +1364,7 @@ public class SeriesServiceTests : AbstractDbTest
|
|||
.Build());
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
|
@ -1387,7 +1396,7 @@ public class SeriesServiceTests : AbstractDbTest
|
|||
.Build());
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
|
@ -1484,4 +1493,116 @@ public class SeriesServiceTests : AbstractDbTest
|
|||
}
|
||||
|
||||
#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
|
||||
|
||||
}
|
||||
|
|
|
@ -130,7 +130,7 @@ public class TachiyomiServiceTests
|
|||
.WithChapter(new ChapterBuilder("96").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("1").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithChapter(new ChapterBuilder("3").WithPages(1).Build())
|
||||
|
@ -175,7 +175,7 @@ public class TachiyomiServiceTests
|
|||
.WithChapter(new ChapterBuilder("96").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("1").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithChapter(new ChapterBuilder("3").WithPages(1).Build())
|
||||
|
@ -265,6 +265,7 @@ public class TachiyomiServiceTests
|
|||
|
||||
Assert.Equal("21", latestChapter.Number);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetLatestChapter_ShouldReturnEncodedVolume_Progress()
|
||||
{
|
||||
|
@ -276,7 +277,7 @@ public class TachiyomiServiceTests
|
|||
.WithChapter(new ChapterBuilder("96").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("1").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithChapter(new ChapterBuilder("21").WithPages(1).Build())
|
||||
|
@ -429,7 +430,7 @@ public class TachiyomiServiceTests
|
|||
.WithChapter(new ChapterBuilder("96").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("1").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithChapter(new ChapterBuilder("3").WithPages(1).Build())
|
||||
|
@ -472,7 +473,7 @@ public class TachiyomiServiceTests
|
|||
.WithChapter(new ChapterBuilder("96").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("1").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithChapter(new ChapterBuilder("3").WithPages(1).Build())
|
||||
|
@ -570,7 +571,7 @@ public class TachiyomiServiceTests
|
|||
.WithChapter(new ChapterBuilder("96").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("1")
|
||||
.WithChapter(new ChapterBuilder("1").WithIsSpecial(true).WithPages(1).Build())
|
||||
.WithChapter(new ChapterBuilder("1").WithPages(1).Build())
|
||||
.Build())
|
||||
.WithVolume(new VolumeBuilder("2")
|
||||
.WithChapter(new ChapterBuilder("21").WithPages(1).Build())
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using API.Extensions;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
|
||||
namespace API.Comparators;
|
||||
|
@ -6,28 +7,28 @@ namespace API.Comparators;
|
|||
#nullable enable
|
||||
|
||||
/// <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>
|
||||
public class ChapterSortComparer : IComparer<double>
|
||||
public class ChapterSortComparerDefaultLast : IComparer<float>
|
||||
{
|
||||
/// <summary>
|
||||
/// Normal sort for 2 doubles. 0 always comes last
|
||||
/// Normal sort for 2 doubles. DefaultChapterNumber always comes last
|
||||
/// </summary>
|
||||
/// <param name="x"></param>
|
||||
/// <param name="y"></param>
|
||||
/// <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 == Parser.DefaultChapterNumber) return 1;
|
||||
if (x.Is(Parser.DefaultChapterNumber)) return 1;
|
||||
// if y is 0, it comes second
|
||||
if (y == Parser.DefaultChapterNumber) return -1;
|
||||
if (y.Is(Parser.DefaultChapterNumber)) return -1;
|
||||
|
||||
return x.CompareTo(y);
|
||||
}
|
||||
|
||||
public static readonly ChapterSortComparer Default = new ChapterSortComparer();
|
||||
public static readonly ChapterSortComparerDefaultLast Default = new ChapterSortComparerDefaultLast();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -37,33 +38,43 @@ public class ChapterSortComparer : IComparer<double>
|
|||
/// This is represented by Chapter 0, Chapter 81.
|
||||
/// </example>
|
||||
/// </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 == Parser.DefaultChapterNumber) return -1;
|
||||
if (x.Is(Parser.DefaultChapterNumber)) return -1;
|
||||
// if y is 0, it comes first
|
||||
if (y == Parser.DefaultChapterNumber) return 1;
|
||||
if (y.Is(Parser.DefaultChapterNumber)) return 1;
|
||||
|
||||
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 0, it comes last
|
||||
if (x == Parser.DefaultChapterNumber) return 1;
|
||||
// if y is 0, it comes last
|
||||
if (y == Parser.DefaultChapterNumber) return -1;
|
||||
if (x.Is(Parser.SpecialVolumeNumber) && y.Is(Parser.SpecialVolumeNumber)) return 0;
|
||||
// if x is 0, it comes second
|
||||
if (x.Is(Parser.SpecialVolumeNumber)) return 1;
|
||||
// if y is 0, it comes second
|
||||
if (y.Is(Parser.SpecialVolumeNumber)) return -1;
|
||||
|
||||
return x.CompareTo(y);
|
||||
}
|
||||
public static readonly SortComparerZeroLast Default = new SortComparerZeroLast();
|
||||
|
||||
public static readonly ChapterSortComparerSpecialsLast Default = new ChapterSortComparerSpecialsLast();
|
||||
}
|
||||
|
|
|
@ -140,7 +140,7 @@ public class DownloadController : BaseApiController
|
|||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume!.SeriesId);
|
||||
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)
|
||||
{
|
||||
|
|
|
@ -70,7 +70,7 @@ public class OpdsController : BaseApiController
|
|||
};
|
||||
|
||||
private readonly FilterV2Dto _filterV2Dto = new FilterV2Dto();
|
||||
private readonly ChapterSortComparer _chapterSortComparer = ChapterSortComparer.Default;
|
||||
private readonly ChapterSortComparerDefaultLast _chapterSortComparerDefaultLast = ChapterSortComparerDefaultLast.Default;
|
||||
private const int PageSize = 20;
|
||||
|
||||
public OpdsController(IUnitOfWork unitOfWork, IDownloadService downloadService,
|
||||
|
@ -857,8 +857,8 @@ public class OpdsController : BaseApiController
|
|||
var seriesDetail = await _seriesService.GetSeriesDetail(seriesId, userId);
|
||||
foreach (var volume in seriesDetail.Volumes)
|
||||
{
|
||||
var chapters = (await _unitOfWork.ChapterRepository.GetChaptersAsync(volume.Id)).OrderBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture),
|
||||
_chapterSortComparer);
|
||||
var chapters = (await _unitOfWork.ChapterRepository.GetChaptersAsync(volume.Id))
|
||||
.OrderBy(x => x.MinNumber, _chapterSortComparerDefaultLast);
|
||||
|
||||
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 volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId);
|
||||
var chapters =
|
||||
(await _unitOfWork.ChapterRepository.GetChaptersAsync(volumeId)).OrderBy(x => double.Parse(x.Number, CultureInfo.InvariantCulture),
|
||||
_chapterSortComparer);
|
||||
(await _unitOfWork.ChapterRepository.GetChaptersAsync(volumeId))
|
||||
.OrderBy(x => x.MinNumber, _chapterSortComparerDefaultLast);
|
||||
var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {_seriesService.FormatChapterName(userId, libraryType)}s ",
|
||||
$"{prefix}{apiKey}/series/{seriesId}/volume/{volumeId}", apiKey, prefix);
|
||||
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}";
|
||||
|
||||
if (volume!.Chapters.Count == 1)
|
||||
if (volume!.Chapters.Count == 1 && !volume.IsSpecial())
|
||||
{
|
||||
var volumeLabel = await _localizationService.Translate(userId, "volume-num", string.Empty);
|
||||
SeriesService.RenameVolumeName(volume.Chapters.First(), volume, libraryType, volumeLabel);
|
||||
if (volume.Name != Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
|
||||
SeriesService.RenameVolumeName(volume, libraryType, volumeLabel);
|
||||
if (!volume.IsLooseLeaf())
|
||||
{
|
||||
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
|
||||
{
|
||||
|
|
|
@ -13,13 +13,17 @@ public class ChapterDto : IHasReadTimeEstimate
|
|||
{
|
||||
public int Id { get; init; }
|
||||
/// <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>
|
||||
public string Range { get; init; } = default!;
|
||||
/// <summary>
|
||||
/// Smallest number of the Range.
|
||||
/// </summary>
|
||||
[Obsolete("Use MinNumber and MaxNumber instead")]
|
||||
public string Number { get; init; } = default!;
|
||||
public float MinNumber { get; init; }
|
||||
public float MaxNumber { get; init; }
|
||||
public float SortOrder { get; init; }
|
||||
/// <summary>
|
||||
/// Total number of pages in all MangaFiles
|
||||
/// </summary>
|
||||
|
|
|
@ -14,5 +14,5 @@ public class ReadHistoryEvent
|
|||
public required string SeriesName { get; set; } = default!;
|
||||
public DateTime ReadDate { get; set; }
|
||||
public int ChapterId { get; set; }
|
||||
public required string ChapterNumber { get; set; } = default!;
|
||||
public required float ChapterNumber { get; set; } = default!;
|
||||
}
|
||||
|
|
12
API/DTOs/TachiyomiChapterDto.cs
Normal file
12
API/DTOs/TachiyomiChapterDto.cs
Normal 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!;
|
||||
}
|
|
@ -3,6 +3,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using API.Entities;
|
||||
using API.Entities.Interfaces;
|
||||
using API.Extensions;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
|
||||
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
|
||||
/// </summary>
|
||||
[Obsolete("Use MinNumber")]
|
||||
public float Number { get; set; }
|
||||
public int Number { get; set; }
|
||||
public int Pages { get; set; }
|
||||
public int PagesRead { get; set; }
|
||||
public DateTime LastModifiedUtc { get; set; }
|
||||
|
@ -50,6 +51,15 @@ public class VolumeDto : IHasReadTimeEstimate
|
|||
/// <returns></returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
entity.Created = DateTime.Now;
|
||||
entity.LastModified = DateTime.Now;
|
||||
entity.CreatedUtc = 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)
|
||||
|
|
140
API/Data/ManualMigrations/ManualMigrateMixedSpecials.cs
Normal file
140
API/Data/ManualMigrations/ManualMigrateMixedSpecials.cs
Normal 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");
|
||||
}
|
||||
}
|
89
API/Data/ManualMigrations/MigrateChapterFields.cs
Normal file
89
API/Data/ManualMigrations/MigrateChapterFields.cs
Normal 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");
|
||||
}
|
||||
}
|
50
API/Data/ManualMigrations/MigrateChapterNumber.cs
Normal file
50
API/Data/ManualMigrations/MigrateChapterNumber.cs
Normal 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");
|
||||
}
|
||||
}
|
|
@ -15,9 +15,8 @@ public static class MigrateLibrariesToHaveAllFileTypes
|
|||
{
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,8 +16,6 @@ public static class MigrateManualHistory
|
|||
{
|
||||
if (await dataContext.ManualMigrationHistory.AnyAsync())
|
||||
{
|
||||
logger.LogCritical(
|
||||
"Running MigrateManualHistory migration - Completed. This is not an error");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ using System.Text.RegularExpressions;
|
|||
using System.Threading.Tasks;
|
||||
using API.DTOs.Filtering.v2;
|
||||
using API.Helpers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
@ -21,8 +22,12 @@ public static class MigrateSmartFilterEncoding
|
|||
|
||||
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();
|
||||
foreach (var filter in smartFilters)
|
||||
|
|
|
@ -14,6 +14,10 @@ public static class MigrateUserLibrarySideNavStream
|
|||
{
|
||||
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
|
||||
.Include(u => u.SideNavStreams)
|
||||
|
|
41
API/Data/ManualMigrations/MigrateVolumeLookupName.cs
Normal file
41
API/Data/ManualMigrations/MigrateVolumeLookupName.cs
Normal 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");
|
||||
}
|
||||
}
|
|
@ -13,8 +13,13 @@ namespace API.Data.ManualMigrations;
|
|||
/// </summary>
|
||||
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))
|
||||
{
|
||||
logger.LogCritical(
|
||||
|
|
|
@ -20,6 +20,11 @@ public static class MigrateWantToReadExport
|
|||
{
|
||||
try
|
||||
{
|
||||
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateWantToReadExport"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var importFile = Path.Join(directoryService.ConfigDirectory, "want-to-read-migration.csv");
|
||||
if (File.Exists(importFile))
|
||||
{
|
||||
|
|
|
@ -6,6 +6,7 @@ using API.Data.Repositories;
|
|||
using API.Entities;
|
||||
using API.Services;
|
||||
using CsvHelper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
@ -15,8 +16,14 @@ namespace API.Data.ManualMigrations;
|
|||
/// </summary>
|
||||
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 outputFile = Path.Join(directoryService.ConfigDirectory, "imported-want-to-read-migration.csv");
|
||||
|
||||
|
|
2877
API/Data/Migrations/20240214232436_ChapterNumber.Designer.cs
generated
Normal file
2877
API/Data/Migrations/20240214232436_ChapterNumber.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
40
API/Data/Migrations/20240214232436_ChapterNumber.cs
Normal file
40
API/Data/Migrations/20240214232436_ChapterNumber.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
2880
API/Data/Migrations/20240216000223_MangaFileNameTemp.Designer.cs
generated
Normal file
2880
API/Data/Migrations/20240216000223_MangaFileNameTemp.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
28
API/Data/Migrations/20240216000223_MangaFileNameTemp.cs
Normal file
28
API/Data/Migrations/20240216000223_MangaFileNameTemp.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
2883
API/Data/Migrations/20240222125420_ChapterIssueSort.Designer.cs
generated
Normal file
2883
API/Data/Migrations/20240222125420_ChapterIssueSort.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
29
API/Data/Migrations/20240222125420_ChapterIssueSort.cs
Normal file
29
API/Data/Migrations/20240222125420_ChapterIssueSort.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
2886
API/Data/Migrations/20240225235816_VolumeLookupName.Designer.cs
generated
Normal file
2886
API/Data/Migrations/20240225235816_VolumeLookupName.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
28
API/Data/Migrations/20240225235816_VolumeLookupName.cs
Normal file
28
API/Data/Migrations/20240225235816_VolumeLookupName.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -679,9 +679,15 @@ namespace API.Data.Migrations
|
|||
b.Property<int>("MaxHoursToRead")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<float>("MaxNumber")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<int>("MinHoursToRead")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<float>("MinNumber")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<string>("Number")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
@ -697,6 +703,9 @@ namespace API.Data.Migrations
|
|||
b.Property<string>("SeriesGroup")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<float>("SortOrder")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<string>("StoryArc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
@ -973,6 +982,9 @@ namespace API.Data.Migrations
|
|||
b.Property<string>("Extension")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FilePath")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
@ -1839,6 +1851,9 @@ namespace API.Data.Migrations
|
|||
b.Property<DateTime>("LastModifiedUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("LookupName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("MaxHoursToRead")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
|
|
@ -167,9 +167,10 @@ public class AppUserProgressRepository : IAppUserProgressRepository
|
|||
(appUserProgresses, chapter) => new {appUserProgresses, chapter})
|
||||
.Where(p => p.appUserProgresses.SeriesId == seriesId && p.appUserProgresses.AppUserId == userId &&
|
||||
p.appUserProgresses.PagesRead >= p.chapter.Pages)
|
||||
.Select(p => p.chapter.Range)
|
||||
.Where(p => p.chapter.MaxNumber != Parser.SpecialVolumeNumber)
|
||||
.Select(p => p.chapter.MaxNumber)
|
||||
.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)
|
||||
|
@ -179,6 +180,7 @@ public class AppUserProgressRepository : IAppUserProgressRepository
|
|||
(appUserProgresses, chapter) => new {appUserProgresses, chapter})
|
||||
.Where(p => p.appUserProgresses.SeriesId == seriesId && p.appUserProgresses.AppUserId == userId &&
|
||||
p.appUserProgresses.PagesRead >= p.chapter.Pages)
|
||||
.Where(p => p.chapter.MaxNumber != Parser.SpecialVolumeNumber)
|
||||
.Select(p => p.chapter.Volume.MaxNumber)
|
||||
.ToListAsync();
|
||||
return list.Count == 0 ? 0 : list.DefaultIfEmpty().Max();
|
||||
|
|
|
@ -78,7 +78,7 @@ public class ChapterRepository : IChapterRepository
|
|||
.Where(c => c.Id == chapterId)
|
||||
.Join(_context.Volume, c => c.VolumeId, v => v.Id, (chapter, volume) => new
|
||||
{
|
||||
ChapterNumber = chapter.Range,
|
||||
ChapterNumber = chapter.MinNumber,
|
||||
VolumeNumber = volume.Name,
|
||||
VolumeId = volume.Id,
|
||||
chapter.IsSpecial,
|
||||
|
@ -102,8 +102,8 @@ public class ChapterRepository : IChapterRepository
|
|||
})
|
||||
.Select(data => new ChapterInfoDto()
|
||||
{
|
||||
ChapterNumber = data.ChapterNumber,
|
||||
VolumeNumber = data.VolumeNumber + string.Empty,
|
||||
ChapterNumber = data.ChapterNumber + string.Empty, // TODO: Fix this
|
||||
VolumeNumber = data.VolumeNumber + string.Empty, // TODO: Fix this
|
||||
VolumeId = data.VolumeId,
|
||||
IsSpecial = data.IsSpecial,
|
||||
SeriesId = data.SeriesId,
|
||||
|
@ -175,6 +175,7 @@ public class ChapterRepository : IChapterRepository
|
|||
{
|
||||
return await _context.Chapter
|
||||
.Includes(includes)
|
||||
.OrderBy(c => c.SortOrder)
|
||||
.FirstOrDefaultAsync(c => c.Id == chapterId);
|
||||
}
|
||||
|
||||
|
@ -187,6 +188,7 @@ public class ChapterRepository : IChapterRepository
|
|||
{
|
||||
return await _context.Chapter
|
||||
.Where(c => c.VolumeId == volumeId)
|
||||
.OrderBy(c => c.SortOrder)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
@ -267,10 +269,16 @@ public class ChapterRepository : IChapterRepository
|
|||
return chapter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Includes Volumes
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<Chapter> GetChaptersForSeries(int seriesId)
|
||||
{
|
||||
return _context.Chapter
|
||||
.Where(c => c.Volume.SeriesId == seriesId)
|
||||
.OrderBy(c => c.SortOrder)
|
||||
.Include(c => c.Volume)
|
||||
.AsEnumerable();
|
||||
}
|
||||
|
|
|
@ -1891,8 +1891,8 @@ public class SeriesRepository : ISeriesRepository
|
|||
VolumeId = c.VolumeId,
|
||||
ChapterId = c.Id,
|
||||
Format = c.Volume.Series.Format,
|
||||
ChapterNumber = c.Number,
|
||||
ChapterRange = c.Range,
|
||||
ChapterNumber = c.MinNumber + string.Empty, // TODO: Refactor this
|
||||
ChapterRange = c.Range, // TODO: Refactor this
|
||||
IsSpecial = c.IsSpecial,
|
||||
VolumeNumber = c.Volume.MinNumber,
|
||||
ChapterTitle = c.Title,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -6,6 +7,7 @@ using API.DTOs;
|
|||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Extensions.QueryExtensions;
|
||||
using API.Services;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
|
@ -14,6 +16,15 @@ using Microsoft.EntityFrameworkCore;
|
|||
|
||||
namespace API.Data.Repositories;
|
||||
|
||||
[Flags]
|
||||
public enum VolumeIncludes
|
||||
{
|
||||
None = 1,
|
||||
Chapters = 2,
|
||||
People = 4,
|
||||
Tags = 8,
|
||||
}
|
||||
|
||||
public interface IVolumeRepository
|
||||
{
|
||||
void Add(Volume volume);
|
||||
|
@ -22,7 +33,7 @@ public interface IVolumeRepository
|
|||
Task<IList<MangaFile>> GetFilesForVolume(int volumeId);
|
||||
Task<string?> GetVolumeCoverImageAsync(int volumeId);
|
||||
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<VolumeDto?> GetVolumeDtoAsync(int volumeId, int userId);
|
||||
Task<IEnumerable<Volume>> GetVolumesForSeriesAsync(IList<int> seriesIds, bool includeChapters = false);
|
||||
|
@ -129,6 +140,7 @@ public class VolumeRepository : IVolumeRepository
|
|||
.Include(vol => vol.Chapters)
|
||||
.ThenInclude(c => c.Files)
|
||||
.AsSplitQuery()
|
||||
.OrderBy(v => v.MinNumber)
|
||||
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
|
||||
.SingleOrDefaultAsync(vol => vol.Id == volumeId);
|
||||
|
||||
|
@ -177,22 +189,22 @@ public class VolumeRepository : IVolumeRepository
|
|||
/// <param name="seriesId"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <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
|
||||
.Where(vol => vol.SeriesId == seriesId)
|
||||
.Include(vol => vol.Chapters)
|
||||
.ThenInclude(c => c.People)
|
||||
.Include(vol => vol.Chapters)
|
||||
.ThenInclude(c => c.Tags)
|
||||
.Includes(includes)
|
||||
.OrderBy(volume => volume.MinNumber)
|
||||
.ProjectTo<VolumeDto>(_mapper.ConfigurationProvider)
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
|
||||
await AddVolumeModifiers(userId, volumes);
|
||||
SortSpecialChapters(volumes);
|
||||
|
||||
foreach (var volume in volumes)
|
||||
{
|
||||
volume.Chapters = volume.Chapters.OrderBy(c => c.SortOrder).ToList();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var volIds = volumes.Select(s => s.Id);
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Interfaces;
|
||||
using API.Extensions;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
|
||||
namespace API.Entities;
|
||||
|
@ -10,14 +12,27 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate
|
|||
{
|
||||
public int Id { get; set; }
|
||||
/// <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>
|
||||
public required string Range { get; set; }
|
||||
/// <summary>
|
||||
/// Smallest number of the Range. Can be a partial like Chapter 4.5
|
||||
/// </summary>
|
||||
[Obsolete("Use MinNumber and MaxNumber instead")]
|
||||
public required string Number { get; set; }
|
||||
/// <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
|
||||
/// </summary>
|
||||
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"/>
|
||||
/// </summary>
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Age Rating for the issue/chapter
|
||||
/// </summary>
|
||||
|
@ -130,10 +146,42 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate
|
|||
if (IsSpecial)
|
||||
{
|
||||
Number = Parser.DefaultChapter;
|
||||
MinNumber = Parser.DefaultChapterNumber;
|
||||
MaxNumber = Parser.DefaultChapterNumber;
|
||||
}
|
||||
Title = (IsSpecial && info.Format == MangaFormat.Epub)
|
||||
? 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,10 @@ public class MangaFile : IEntityDate
|
|||
{
|
||||
public int Id { get; set; }
|
||||
/// <summary>
|
||||
/// The filename without extension
|
||||
/// </summary>
|
||||
public string FileName { get; set; }
|
||||
/// <summary>
|
||||
/// Absolute path to the archive file
|
||||
/// </summary>
|
||||
public required string FilePath { get; set; }
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using API.Entities.Interfaces;
|
||||
using API.Extensions;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
|
||||
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>
|
||||
public required string Name { get; set; }
|
||||
/// <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
|
||||
/// </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>
|
||||
|
@ -55,4 +61,17 @@ public class Volume : IEntityDate, IHasReadTimeEstimate
|
|||
public Series Series { get; set; } = null!;
|
||||
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}";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using API.Entities;
|
||||
using API.Helpers;
|
||||
|
@ -30,7 +31,7 @@ public static class ChapterListExtensions
|
|||
{
|
||||
var specialTreatment = info.IsSpecialInfo();
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
26
API/Extensions/FloatExtensions.cs
Normal file
26
API/Extensions/FloatExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
|
@ -39,6 +39,31 @@ public static class IncludesExtensions
|
|||
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,
|
||||
SeriesIncludes includeFlags)
|
||||
{
|
||||
|
|
|
@ -19,13 +19,13 @@ public static class SeriesExtensions
|
|||
public static string? GetCoverImage(this Series series)
|
||||
{
|
||||
var volumes = (series.Volumes ?? [])
|
||||
.OrderBy(v => v.MinNumber, ChapterSortComparer.Default)
|
||||
.OrderBy(v => v.MinNumber, ChapterSortComparerDefaultLast.Default)
|
||||
.ToList();
|
||||
var firstVolume = volumes.GetCoverImage(series.Format);
|
||||
if (firstVolume == null) return null;
|
||||
|
||||
var chapters = firstVolume.Chapters
|
||||
.OrderBy(c => c.Number.AsDouble(), ChapterSortComparerZeroFirst.Default)
|
||||
.OrderBy(c => c.SortOrder, ChapterSortComparerDefaultLast.Default)
|
||||
.ToList();
|
||||
|
||||
if (chapters.Count > 1 && chapters.Exists(c => c.IsSpecial))
|
||||
|
@ -41,25 +41,36 @@ public static class SeriesExtensions
|
|||
// If we have loose leaf chapters
|
||||
|
||||
// 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)
|
||||
.SelectMany(c => c.Chapters.Where(c => !c.IsSpecial))
|
||||
.OrderBy(c => c.Number.AsDouble(), ChapterSortComparerZeroFirst.Default)
|
||||
var looseLeafChapters = volumes.Where(v => v.MinNumber.Is(Parser.LooseLeafVolumeNumber))
|
||||
.SelectMany(c => c.Chapters.Where(c2 => !c2.IsSpecial))
|
||||
.OrderBy(c => c.MinNumber, ChapterSortComparerDefaultFirst.Default)
|
||||
.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 firstVolume.CoverImage;
|
||||
}
|
||||
|
||||
var firstLooseLeafChapter = volumes
|
||||
.Where(v => $"{v.MinNumber}" == Parser.LooseLeafVolume)
|
||||
.SelectMany(v => v.Chapters)
|
||||
.OrderBy(c => c.Number.AsDouble(), ChapterSortComparerZeroFirst.Default)
|
||||
.FirstOrDefault(c => !c.IsSpecial);
|
||||
var chpts = volumes
|
||||
.First(v => v.MinNumber.Is(Parser.LooseLeafVolumeNumber))
|
||||
.Chapters
|
||||
//.Where(v => v.MinNumber.Is(Parser.LooseLeafVolumeNumber))
|
||||
//.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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.Collections;
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using API.Comparators;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
|
@ -24,7 +25,7 @@ public static class VolumeListExtensions
|
|||
{
|
||||
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);
|
||||
}
|
||||
|
@ -45,7 +46,7 @@ public static class VolumeListExtensions
|
|||
/// <returns></returns>
|
||||
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>
|
||||
|
@ -55,7 +56,8 @@ public static class VolumeListExtensions
|
|||
/// <returns></returns>
|
||||
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>
|
||||
|
@ -65,16 +67,16 @@ public static class VolumeListExtensions
|
|||
/// <returns></returns>
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
return volumes.Where(v => Math.Abs(v.MinNumber - Parser.DefaultChapterNumber) < 0.001f);
|
||||
return volumes.Where(v => v.MinNumber.Is(Parser.DefaultChapterNumber));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ public class AutoMapperProfiles : Profile
|
|||
.ForMember(dest => dest.Series, opt => opt.MapFrom(src => src.Series));
|
||||
CreateMap<LibraryDto, Library>();
|
||||
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<Chapter, ChapterDto>();
|
||||
CreateMap<Series, SeriesDto>();
|
||||
|
@ -200,6 +200,8 @@ public class AutoMapperProfiles : Profile
|
|||
CreateMap<ReadingList, ReadingListDto>();
|
||||
CreateMap<ReadingListItem, ReadingListItemDto>();
|
||||
CreateMap<ScrobbleError, ScrobbleErrorDto>();
|
||||
CreateMap<ChapterDto, TachiyomiChapterDto>();
|
||||
CreateMap<Chapter, TachiyomiChapterDto>();
|
||||
|
||||
CreateMap<Series, SearchResultDto>()
|
||||
.ForMember(dest => dest.SeriesId,
|
||||
|
|
|
@ -20,8 +20,12 @@ public class ChapterBuilder : IEntityBuilder<Chapter>
|
|||
Range = string.IsNullOrEmpty(range) ? number : range,
|
||||
Title = string.IsNullOrEmpty(range) ? number : range,
|
||||
Number = Parser.MinNumberFromRange(number).ToString(CultureInfo.InvariantCulture),
|
||||
MinNumber = Parser.MinNumberFromRange(number),
|
||||
MaxNumber = Parser.MaxNumberFromRange(number),
|
||||
SortOrder = Parser.MinNumberFromRange(number),
|
||||
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 specialTitle = specialTreatment ? info.Filename : info.Chapters;
|
||||
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)
|
||||
.WithRange(specialTreatment ? info.Filename : info.Chapters)
|
||||
.WithTitle((specialTreatment && info.Format == MangaFormat.Epub)
|
||||
? info.Title
|
||||
: specialTitle)
|
||||
// NEW
|
||||
//.WithTitle(string.IsNullOrEmpty(info.Filename) ? specialTitle : info.Filename)
|
||||
.WithTitle(info.Filename)
|
||||
.WithIsSpecial(specialTreatment);
|
||||
}
|
||||
|
||||
|
@ -44,9 +52,18 @@ public class ChapterBuilder : IEntityBuilder<Chapter>
|
|||
return this;
|
||||
}
|
||||
|
||||
|
||||
public ChapterBuilder WithNumber(string 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;
|
||||
}
|
||||
|
||||
|
@ -65,6 +82,8 @@ public class ChapterBuilder : IEntityBuilder<Chapter>
|
|||
private ChapterBuilder WithRange(string range)
|
||||
{
|
||||
_chapter.Range = range;
|
||||
// TODO: HACK: Overriding range
|
||||
_chapter.Range = _chapter.GetNumberTitle();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ public class MangaFileBuilder : IEntityBuilder<MangaFile>
|
|||
Pages = pages,
|
||||
LastModified = File.GetLastWriteTime(filePath),
|
||||
LastModifiedUtc = File.GetLastWriteTimeUtc(filePath),
|
||||
FileName = Path.GetFileNameWithoutExtension(filePath)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,9 @@ public class SeriesBuilder : IEntityBuilder<Series>
|
|||
SortName = name,
|
||||
NormalizedName = name.ToNormalized(),
|
||||
NormalizedLocalizedName = name.ToNormalized(),
|
||||
Metadata = new SeriesMetadataBuilder().Build(),
|
||||
Metadata = new SeriesMetadataBuilder()
|
||||
.WithPublicationStatus(PublicationStatus.OnGoing)
|
||||
.Build(),
|
||||
Volumes = new List<Volume>(),
|
||||
ExternalSeriesMetadata = new ExternalSeriesMetadata()
|
||||
};
|
||||
|
@ -90,4 +92,10 @@ public class SeriesBuilder : IEntityBuilder<Series>
|
|||
_series.LibraryId = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SeriesBuilder WithPublicationStatus(PublicationStatus status)
|
||||
{
|
||||
_series.Metadata.PublicationStatus = status;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ public class VolumeBuilder : IEntityBuilder<Volume>
|
|||
_volume = new Volume()
|
||||
{
|
||||
Name = volumeNumber,
|
||||
LookupName = volumeNumber,
|
||||
MinNumber = Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(volumeNumber),
|
||||
MaxNumber = Services.Tasks.Scanner.Parser.Parser.MaxNumberFromRange(volumeNumber),
|
||||
Chapters = new List<Chapter>()
|
||||
|
@ -49,7 +50,7 @@ public class VolumeBuilder : IEntityBuilder<Volume>
|
|||
return this;
|
||||
}
|
||||
|
||||
public VolumeBuilder WithChapters(List<Chapter> chapters)
|
||||
public VolumeBuilder WithChapters(IList<Chapter> chapters)
|
||||
{
|
||||
_volume.Chapters = chapters;
|
||||
return this;
|
||||
|
|
|
@ -657,7 +657,7 @@ public class DirectoryService : IDirectoryService
|
|||
/// <returns></returns>
|
||||
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>();
|
||||
if (!Exists(folderPath)) return files;
|
||||
|
||||
|
|
|
@ -197,7 +197,7 @@ public class MediaConversionService : IMediaConversionService
|
|||
foreach (var volume in nonCustomOrConvertedVolumeCovers)
|
||||
{
|
||||
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);
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
|
|
@ -108,7 +108,7 @@ public class MetadataService : IMetadataService
|
|||
|
||||
|
||||
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);
|
||||
|
||||
volume.CoverImage = firstChapter.CoverImage;
|
||||
|
|
|
@ -11,7 +11,9 @@ using API.DTOs.Scrobbling;
|
|||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Scrobble;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using API.SignalR;
|
||||
using Flurl.Http;
|
||||
using Hangfire;
|
||||
|
@ -330,6 +332,15 @@ public class ScrobblingService : IScrobblingService
|
|||
await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadChapterForSeries(seriesId, userId),
|
||||
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);
|
||||
await _unitOfWork.CommitAsync();
|
||||
_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
|
||||
});
|
||||
evt.IsErrored = true;
|
||||
evt.ErrorDetails = "Series cannot be matched for Scrobbling";
|
||||
evt.ErrorDetails = UnknownSeriesErrorMessage;
|
||||
evt.ProcessDateUtc = DateTime.UtcNow;
|
||||
_unitOfWork.ScrobbleRepository.Update(evt);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
|
|
@ -51,8 +51,9 @@ public class ReaderService : IReaderService
|
|||
private readonly IImageService _imageService;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly IScrobblingService _scrobblingService;
|
||||
private readonly ChapterSortComparer _chapterSortComparer = ChapterSortComparer.Default;
|
||||
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = ChapterSortComparerZeroFirst.Default;
|
||||
private readonly ChapterSortComparerDefaultLast _chapterSortComparerDefaultLast = ChapterSortComparerDefaultLast.Default;
|
||||
private readonly ChapterSortComparerDefaultFirst _chapterSortComparerForInChapterSorting = ChapterSortComparerDefaultFirst.Default;
|
||||
private readonly ChapterSortComparerSpecialsLast _chapterSortComparerSpecialsLast = ChapterSortComparerSpecialsLast.Default;
|
||||
|
||||
private const float MinWordsPerHour = 10260F;
|
||||
private const float MaxWordsPerHour = 30000F;
|
||||
|
@ -346,11 +347,23 @@ public class ReaderService : IReaderService
|
|||
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>
|
||||
/// Tries to find the next logical Chapter
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <param name="volumeId"></param>
|
||||
|
@ -359,112 +372,88 @@ public class ReaderService : IReaderService
|
|||
/// <returns>-1 if nothing can be found</returns>
|
||||
public async Task<int> GetNextChapterIdAsync(int seriesId, int volumeId, int currentChapterId, int userId)
|
||||
{
|
||||
var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId))
|
||||
.ToList();
|
||||
var currentVolume = volumes.Single(v => v.Id == volumeId);
|
||||
var currentChapter = currentVolume.Chapters.Single(c => c.Id == currentChapterId);
|
||||
var volumes = await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId);
|
||||
|
||||
var currentVolume = volumes.FirstOrDefault(v => v.Id == volumeId);
|
||||
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())
|
||||
{
|
||||
// Handle specials by sorting on their Filename aka Range
|
||||
var chapterId = GetNextChapterId(currentVolume.Chapters.OrderByNatural(x => x.Range), currentChapter.Range, dto => dto.Range);
|
||||
// Handle loose-leaf chapters
|
||||
chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => x.SortOrder),
|
||||
currentChapter.SortOrder,
|
||||
dto => dto.SortOrder);
|
||||
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;
|
||||
foreach (var volume in volumes)
|
||||
// Check within the current volume if the next chapter within it can be next
|
||||
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;
|
||||
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;
|
||||
}
|
||||
return chapters[currentChapterIndex + 1].Id;
|
||||
}
|
||||
|
||||
// Check within the current Volume
|
||||
chapterId = GetNextChapterId(chapters, currentChapter.SortOrder, dto => dto.SortOrder);
|
||||
if (chapterId > 0) return chapterId;
|
||||
|
||||
|
||||
// If we are the last volume and we didn't find any next volume, loop back to volume 0 and give the first chapter
|
||||
// This has an added problem that it will loop up to the beginning always
|
||||
// Should I change this to Max number? volumes.LastOrDefault()?.Number -> volumes.Max(v => v.Number)
|
||||
|
||||
if (!currentVolume.IsLooseLeaf() && currentVolume.MinNumber == volumes.LastOrDefault()?.MinNumber && volumes.Count > 1)
|
||||
// Now check the next volume
|
||||
var nextVolumeIndex = currentVolumeIndex + 1;
|
||||
if (nextVolumeIndex < volumes.Count)
|
||||
{
|
||||
var chapterVolume = volumes.FirstOrDefault();
|
||||
if (chapterVolume == null || !chapterVolume.IsLooseLeaf()) return -1;
|
||||
// Get the first chapter from the next volume
|
||||
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
|
||||
// var orderedVolumes = volumes.OrderBy(v => v.Number, SortComparerZeroLast.Default).ToList();
|
||||
// if (currentVolume.Number == orderedVolumes.FirstOrDefault().Number)
|
||||
// {
|
||||
// // We can move into loose leaf chapters
|
||||
// //var firstLooseLeaf = volumes.LastOrDefault().Chapters.MinBy(x => x.Number.AsDouble(), _chapterSortComparer);
|
||||
// var nextChapterId = GetNextChapterId(
|
||||
// volumes.LastOrDefault().Chapters.OrderBy(x => x.Number.AsDouble(), _chapterSortComparer),
|
||||
// "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;
|
||||
// We are the last volume, so we need to check loose leaf
|
||||
if (currentVolumeIndex == volumes.Count - 1)
|
||||
{
|
||||
// Try to find the first loose-leaf chapter in this volume
|
||||
var firstLooseLeafChapter = volumes.WhereLooseLeaf().FirstOrDefault()?.Chapters.MinBy(c => c.MinNumber, _chapterSortComparerForInChapterSorting);
|
||||
if (firstLooseLeafChapter != null)
|
||||
{
|
||||
return firstLooseLeafChapter.Id;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to find the prev logical Chapter
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <param name="volumeId"></param>
|
||||
|
@ -473,52 +462,76 @@ public class ReaderService : IReaderService
|
|||
/// <returns>-1 if nothing can be found</returns>
|
||||
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 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,
|
||||
dto => dto.Range);
|
||||
// Check within Specials, if not set the currentVolume to Loose Leaf
|
||||
chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => x.SortOrder).Reverse(),
|
||||
currentChapter.SortOrder,
|
||||
dto => dto.SortOrder);
|
||||
if (chapterId > 0) return chapterId;
|
||||
currentVolume = volumes.FirstOrDefault(v => v.IsLooseLeaf());
|
||||
}
|
||||
|
||||
var next = false;
|
||||
foreach (var volume in volumes)
|
||||
if (currentVolume != null && currentVolume.IsLooseLeaf())
|
||||
{
|
||||
if (volume.MinNumber == currentVolume.MinNumber)
|
||||
{
|
||||
var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => x.Number.AsDouble(), _chapterSortComparerForInChapterSorting).Reverse(),
|
||||
currentChapter.Range, dto => dto.Range);
|
||||
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
|
||||
continue;
|
||||
}
|
||||
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;
|
||||
}
|
||||
// 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();
|
||||
chapterId = GetPrevChapterId(currentVolumeChapters,
|
||||
currentChapter.SortOrder, dto => dto.SortOrder, c => c.Id);
|
||||
if (chapterId > 0) return chapterId;
|
||||
currentVolume = volumes.FindLast(v => !v.IsLooseLeaf() && !v.IsSpecial());
|
||||
if (currentVolume != null) return currentVolume.Chapters.OrderBy(x => x.SortOrder).Last()?.Id ?? -1;
|
||||
}
|
||||
|
||||
var lastVolume = volumes.MaxBy(v => v.MinNumber);
|
||||
if (currentVolume.IsLooseLeaf() && currentVolume.MinNumber != lastVolume?.MinNumber && lastVolume?.Chapters.Count > 1)
|
||||
// When we started as a special and there was no loose leafs, reset the currentVolume
|
||||
if (currentVolume == null)
|
||||
{
|
||||
var lastChapter = lastVolume.Chapters.MaxBy(x => x.Number.AsDouble(), _chapterSortComparerForInChapterSorting);
|
||||
if (lastChapter == null) return -1;
|
||||
return lastChapter.Id;
|
||||
currentVolume = volumes.FirstOrDefault(v => !v.IsLooseLeaf() && !v.IsSpecial());
|
||||
if (currentVolume == null) return -1;
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <param name="userId"></param>
|
||||
|
@ -527,28 +540,42 @@ public class ReaderService : IReaderService
|
|||
{
|
||||
var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId)).ToList();
|
||||
|
||||
if (!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();
|
||||
var anyUserProgress =
|
||||
await _unitOfWork.AppUserProgressRepository.AnyUserProgressForSeriesAsync(seriesId, userId);
|
||||
|
||||
// If there are specials, then return the first Non-special
|
||||
if (chapters.Exists(c => c.IsSpecial))
|
||||
{
|
||||
var firstChapter = chapters.FirstOrDefault(c => !c.IsSpecial);
|
||||
if (firstChapter == null)
|
||||
{
|
||||
// If there is no non-special chapter, then return first chapter
|
||||
return chapters[0];
|
||||
}
|
||||
if (!anyUserProgress)
|
||||
{
|
||||
// I think i need a way to sort volumes last
|
||||
volumes = volumes.OrderBy(v => v.MinNumber, _chapterSortComparerSpecialsLast).ToList();
|
||||
|
||||
return firstChapter;
|
||||
}
|
||||
// Else use normal logic
|
||||
return chapters[0];
|
||||
}
|
||||
// Check if we have a non-loose leaf volume
|
||||
var nonLooseLeafNonSpecialVolume = volumes.Find(v => !v.IsLooseLeaf() && !v.IsSpecial());
|
||||
if (nonLooseLeafNonSpecialVolume != null)
|
||||
{
|
||||
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
|
||||
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
|
||||
// If there are any volumes that have progress, return those. If not, move on.
|
||||
var currentlyReadingChapter = volumeChapters
|
||||
.OrderBy(c => c.Number.AsDouble(), _chapterSortComparer)
|
||||
.OrderBy(c => c.MinNumber, _chapterSortComparerDefaultLast)
|
||||
.FirstOrDefault(chapter => chapter.PagesRead < chapter.Pages && chapter.PagesRead > 0);
|
||||
if (currentlyReadingChapter != null) return currentlyReadingChapter;
|
||||
|
||||
// Order with volume 0 last so we prefer the natural order
|
||||
return FindNextReadingChapter(volumes.OrderBy(v => v.MinNumber, SortComparerZeroLast.Default)
|
||||
.SelectMany(v => v.Chapters.OrderBy(c => c.Number.AsDouble()))
|
||||
return FindNextReadingChapter(volumes.OrderBy(v => v.MinNumber, _chapterSortComparerDefaultLast)
|
||||
.SelectMany(v => v.Chapters.OrderBy(c => c.SortOrder))
|
||||
.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 chaptersList = chapters.ToList();
|
||||
|
@ -636,8 +663,8 @@ public class ReaderService : IReaderService
|
|||
foreach (var volume in volumes.OrderBy(v => v.MinNumber))
|
||||
{
|
||||
var chapters = volume.Chapters
|
||||
.Where(c => !c.IsSpecial && Parser.MaxNumberFromRange(c.Range) <= chapterNumber)
|
||||
.OrderBy(c => c.Number.AsFloat());
|
||||
.Where(c => !c.IsSpecial && c.MaxNumber <= chapterNumber)
|
||||
.OrderBy(c => c.MinNumber);
|
||||
await MarkChaptersAsRead(user, volume.SeriesId, chapters.ToList());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ public class ReadingListService : IReadingListService
|
|||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ILogger<ReadingListService> _logger;
|
||||
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,
|
||||
Parser.RegexTimeout);
|
||||
|
||||
|
@ -391,8 +391,8 @@ public class ReadingListService : IReadingListService
|
|||
|
||||
var existingChapterExists = readingList.Items.Select(rli => rli.ChapterId).ToHashSet();
|
||||
var chaptersForSeries = (await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds, ChapterIncludes.Volumes))
|
||||
.OrderBy(c => Parser.MinNumberFromRange(c.Volume.Name))
|
||||
.ThenBy(x => x.Number.AsDouble(), _chapterSortComparerForInChapterSorting)
|
||||
.OrderBy(c => c.Volume.MinNumber)
|
||||
.ThenBy(x => x.MinNumber, _chapterSortComparerForInChapterSorting)
|
||||
.ToList();
|
||||
|
||||
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
|
||||
var bookNumber = string.IsNullOrEmpty(book.Number)
|
||||
? Parser.DefaultChapter
|
||||
: book.Number;
|
||||
var chapter = matchingVolume.Chapters.FirstOrDefault(c => c.Number == bookNumber);
|
||||
? Parser.DefaultChapterNumber
|
||||
: float.Parse(book.Number);
|
||||
var chapter = matchingVolume.Chapters.FirstOrDefault(c => c.MinNumber.Is(bookNumber));
|
||||
if (chapter == null)
|
||||
{
|
||||
importSummary.Results.Add(new CblBookResult(book)
|
||||
|
|
|
@ -59,7 +59,7 @@ public class SeriesService : ISeriesService
|
|||
{
|
||||
ExpectedDate = null,
|
||||
ChapterNumber = 0,
|
||||
VolumeNumber = 0
|
||||
VolumeNumber = Parser.LooseLeafVolumeNumber
|
||||
};
|
||||
|
||||
public SeriesService(IUnitOfWork unitOfWork, IEventHub eventHub, ITaskScheduler taskScheduler,
|
||||
|
@ -81,21 +81,21 @@ public class SeriesService : ISeriesService
|
|||
public static Chapter? GetFirstChapterForMetadata(Series series)
|
||||
{
|
||||
var sortedVolumes = series.Volumes
|
||||
.Where(v => float.TryParse(v.Name, CultureInfo.InvariantCulture, out var parsedValue) && parsedValue != Parser.LooseLeafVolumeNumber)
|
||||
.OrderBy(v => float.TryParse(v.Name, CultureInfo.InvariantCulture, out var parsedValue) ? parsedValue : float.MaxValue);
|
||||
.Where(v => v.MinNumber.IsNot(Parser.LooseLeafVolumeNumber))
|
||||
.OrderBy(v => v.MinNumber);
|
||||
var minVolumeNumber = sortedVolumes.MinBy(v => v.MinNumber);
|
||||
|
||||
|
||||
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();
|
||||
var minChapter = allChapters
|
||||
.FirstOrDefault();
|
||||
|
||||
if (minVolumeNumber != null && minChapter != null && float.TryParse(minChapter.Number, CultureInfo.InvariantCulture, out var chapNum) &&
|
||||
(chapNum >= minVolumeNumber.MinNumber || chapNum == Parser.DefaultChapterNumber))
|
||||
if (minVolumeNumber != null && minChapter != null &&
|
||||
(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;
|
||||
|
@ -481,74 +481,63 @@ public class SeriesService : ISeriesService
|
|||
|
||||
|
||||
var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId);
|
||||
var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId))
|
||||
.OrderBy(v => Parser.MinNumberFromRange(v.Name))
|
||||
.ToList();
|
||||
var bookTreatment = libraryType is LibraryType.Book or LibraryType.LightNovel;
|
||||
var volumeLabel = await _localizationService.Translate(userId, "volume-num", string.Empty);
|
||||
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.
|
||||
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);
|
||||
foreach (var volume in volumes)
|
||||
if (volume.IsLooseLeaf() || volume.IsSpecial())
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
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 chapters = volumes.SelectMany(v => v.Chapters.Select(c =>
|
||||
{
|
||||
if (v.IsLooseLeaf()) return c;
|
||||
c.VolumeTitle = v.Name;
|
||||
return c;
|
||||
}).OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default)).ToList();
|
||||
// Why isn't this doing a check if chapter is not special as it wont get included
|
||||
var chapters = volumes
|
||||
.SelectMany(v => v.Chapters
|
||||
.Select(c =>
|
||||
{
|
||||
if (v.IsLooseLeaf()) return c;
|
||||
c.VolumeTitle = v.Name;
|
||||
return c;
|
||||
})
|
||||
.OrderBy(c => c.SortOrder))
|
||||
.ToList();
|
||||
|
||||
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;
|
||||
else chapter.Title = await FormatChapterTitle(userId, chapter, libraryType);
|
||||
|
||||
if (!chapter.IsSpecial) continue;
|
||||
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)
|
||||
IEnumerable<ChapterDto> retChapters;
|
||||
if (libraryType is LibraryType.Book or LibraryType.LightNovel)
|
||||
{
|
||||
retChapters = Array.Empty<ChapterDto>();
|
||||
} else
|
||||
{
|
||||
retChapters = chapters
|
||||
.Where(ShouldIncludeChapter);
|
||||
}
|
||||
IEnumerable<ChapterDto> retChapters = bookTreatment ? Array.Empty<ChapterDto>() : chapters.Where(ShouldIncludeChapter);
|
||||
|
||||
var storylineChapters = volumes
|
||||
.WhereLooseLeaf()
|
||||
.SelectMany(v => v.Chapters.Where(c => !c.IsSpecial))
|
||||
.OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default)
|
||||
.OrderBy(c => c.SortOrder)
|
||||
.ToList();
|
||||
|
||||
// When there's chapters without a volume number revert to chapter sorting only as opposed to volume then chapter
|
||||
if (storylineChapters.Any()) {
|
||||
retChapters = retChapters.OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default);
|
||||
if (storylineChapters.Count > 0) {
|
||||
retChapters = retChapters.OrderBy(c => c.SortOrder, ChapterSortComparerDefaultLast.Default);
|
||||
}
|
||||
|
||||
return new SeriesDetailDto
|
||||
|
@ -569,35 +558,35 @@ public class SeriesService : ISeriesService
|
|||
/// <returns></returns>
|
||||
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)
|
||||
{
|
||||
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 (firstChapter.Range.Equals(Parser.LooseLeafVolume)) return;
|
||||
if (firstChapter.Range.Equals(Parser.LooseLeafVolume)) return false;
|
||||
var title = Path.GetFileNameWithoutExtension(firstChapter.Range);
|
||||
if (string.IsNullOrEmpty(title)) return;
|
||||
volume.Name += $" - {title}";
|
||||
if (string.IsNullOrEmpty(title)) return false;
|
||||
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?
|
||||
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;
|
||||
|
||||
// For number and volume number, we need the highest chapter, not the latest created
|
||||
var lastChapter = chapters.MaxBy(c => c.Number.AsFloat())!;
|
||||
float.TryParse(lastChapter.Number, NumberStyles.Number, CultureInfo.InvariantCulture,
|
||||
out var lastChapterNumber);
|
||||
var lastChapter = chapters.MaxBy(c => c.MaxNumber)!;
|
||||
var lastChapterNumber = lastChapter.MaxNumber;
|
||||
|
||||
var lastVolumeNum = chapters.Select(c => c.Volume.MinNumber).Max();
|
||||
|
||||
var result = new NextExpectedChapterDto
|
||||
{
|
||||
ChapterNumber = 0,
|
||||
VolumeNumber = 0,
|
||||
VolumeNumber = Parser.LooseLeafVolumeNumber,
|
||||
ExpectedDate = nextChapterExpected,
|
||||
Title = string.Empty
|
||||
};
|
||||
|
|
|
@ -336,7 +336,7 @@ public class StatisticService : IStatisticService
|
|||
LibraryId = u.LibraryId,
|
||||
ReadDate = u.LastModified,
|
||||
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)
|
||||
.ToListAsync();
|
||||
|
|
|
@ -14,10 +14,11 @@ using AutoMapper;
|
|||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Services;
|
||||
#nullable enable
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -51,7 +52,7 @@ public class TachiyomiService : ITachiyomiService
|
|||
/// 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.
|
||||
/// 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);
|
||||
|
||||
|
@ -74,50 +75,48 @@ public class TachiyomiService : ITachiyomiService
|
|||
{
|
||||
var volumeChapter = _mapper.Map<ChapterDto>(volumes
|
||||
[^1].Chapters
|
||||
.OrderBy(c => c.Number.AsFloat(), ChapterSortComparerZeroFirst.Default)
|
||||
.OrderBy(c => c.MinNumber, ChapterSortComparerDefaultFirst.Default)
|
||||
.Last());
|
||||
if (volumeChapter.Number == Parser.LooseLeafVolume)
|
||||
|
||||
if (volumeChapter.MinNumber.Is(Parser.LooseLeafVolumeNumber))
|
||||
{
|
||||
var volume = volumes.First(v => v.Id == volumeChapter.VolumeId);
|
||||
return new ChapterDto()
|
||||
{
|
||||
// Use R to ensure that localization of underlying system doesn't affect the stringification
|
||||
// https://docs.microsoft.com/en-us/globalization/locale/number-formatting-in-dotnet-framework
|
||||
Number = (volume.MinNumber / 10_000f).ToString("R", EnglishCulture)
|
||||
};
|
||||
return CreateTachiyomiChapterDto(volume.MinNumber);
|
||||
}
|
||||
|
||||
return new ChapterDto()
|
||||
{
|
||||
Number = (int.Parse(volumeChapter.Number) / 10_000f).ToString("R", EnglishCulture)
|
||||
};
|
||||
return CreateTachiyomiChapterDto(volumeChapter.MinNumber);
|
||||
}
|
||||
|
||||
var lastChapter = looseLeafChapterVolume.Chapters
|
||||
.OrderBy(c => double.Parse(c.Number, CultureInfo.InvariantCulture), ChapterSortComparer.Default)
|
||||
.OrderBy(c => c.MinNumber, ChapterSortComparerDefaultLast.Default)
|
||||
.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.
|
||||
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
|
||||
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
|
||||
return new ChapterDto()
|
||||
{
|
||||
// Use R to ensure that localization of underlying system doesn't affect the stringification
|
||||
// https://docs.microsoft.com/en-us/globalization/locale/number-formatting-in-dotnet-framework
|
||||
Number = (volumeWithProgress.MinNumber / 10_000f).ToString("R", EnglishCulture)
|
||||
|
||||
};
|
||||
return CreateTachiyomiChapterDto(volumeWithProgress.MinNumber);
|
||||
}
|
||||
|
||||
// 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>
|
||||
|
|
|
@ -365,12 +365,68 @@ public class ParseScannedFiles
|
|||
|
||||
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>
|
||||
|
|
|
@ -96,6 +96,7 @@ public class DefaultParser : IDefaultParser
|
|||
if (Parser.HasSpecialMarker(fileName))
|
||||
{
|
||||
ret.IsSpecial = true;
|
||||
ret.SpecialIndex = Parser.ParseSpecialIndex(fileName);
|
||||
ret.Chapters = Parser.DefaultChapter;
|
||||
ret.Volumes = Parser.LooseLeafVolume;
|
||||
|
||||
|
@ -113,6 +114,12 @@ public class DefaultParser : IDefaultParser
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
@ -12,10 +13,16 @@ namespace API.Services.Tasks.Scanner.Parser;
|
|||
public static class Parser
|
||||
{
|
||||
// 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 LooseLeafVolume = "0";
|
||||
public const int DefaultChapterNumber = 0;
|
||||
public const int LooseLeafVolumeNumber = 0;
|
||||
public const string DefaultChapter = "-100000"; // -2147483648
|
||||
public const string LooseLeafVolume = "-100000";
|
||||
public const int DefaultChapterNumber = -100_000;
|
||||
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 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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
filePath = ReplaceUnderscores(filePath);
|
||||
|
@ -944,35 +958,52 @@ public static class Parser
|
|||
{
|
||||
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("-");
|
||||
return tokens.Min(t => t.AsFloat());
|
||||
// Check if there is a range or not
|
||||
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)
|
||||
{
|
||||
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("-");
|
||||
return tokens.Max(t => t.AsFloat());
|
||||
// Check if there is a range or not
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -60,6 +60,10 @@ public class ParserInfo
|
|||
/// If the file contains no volume/chapter information or contains Special Keywords <see cref="Parser.MangaSpecialRegex"/>
|
||||
/// </summary>
|
||||
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>
|
||||
/// Used for specials or books, stores what the UI should show.
|
||||
|
@ -67,6 +71,12 @@ public class ParserInfo
|
|||
/// </summary>
|
||||
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>
|
||||
/// If the ParserInfo has the IsSpecial tag or both volumes and chapters are default aka 0
|
||||
/// </summary>
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
|
@ -219,14 +220,6 @@ public class ProcessSeries : IProcessSeries
|
|||
_logger.LogCritical(ex,
|
||||
"[ScannerService] There was an issue writing to the database for series {SeriesName}",
|
||||
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,
|
||||
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
|
||||
series.Metadata.MaxCount = chapters.Max(chapter => chapter.Count);
|
||||
|
||||
var maxVolume = series.Volumes.Max(v => (int) Parser.Parser.MaxNumberFromRange(v.Name));
|
||||
var maxChapter = chapters.Max(c => (int) Parser.Parser.MaxNumberFromRange(c.Range));
|
||||
var maxVolume = (int) series.Volumes.Max(v => v.MaxNumber);
|
||||
var maxChapter = (int) chapters.Max(c => c.MaxNumber);
|
||||
|
||||
// Single books usually don't have a number in their Range (filename)
|
||||
if (series.Format == MangaFormat.Epub || series.Format == MangaFormat.Pdf && chapters.Count == 1)
|
||||
|
@ -544,10 +537,12 @@ public class ProcessSeries : IProcessSeries
|
|||
Volume? volume;
|
||||
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)
|
||||
{
|
||||
// TODO: Push this to UI in some way
|
||||
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);
|
||||
throw new KavitaException(
|
||||
|
@ -561,7 +556,8 @@ public class ProcessSeries : IProcessSeries
|
|||
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);
|
||||
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
|
||||
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)
|
||||
{
|
||||
_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;
|
||||
if (!string.IsNullOrEmpty(file) && _directoryService.FileSystem.File.Exists(file))
|
||||
{
|
||||
// This can happen when file is renamed and volume is removed
|
||||
_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);
|
||||
}
|
||||
|
||||
|
@ -640,12 +639,19 @@ public class ProcessSeries : IProcessSeries
|
|||
chapter.UpdateFrom(info);
|
||||
}
|
||||
|
||||
if (chapter == null) continue;
|
||||
if (chapter == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Add files
|
||||
var specialTreatment = info.IsSpecialInfo();
|
||||
AddOrUpdateFileForChapter(chapter, info, forceUpdate);
|
||||
|
||||
// TODO: Investigate using the ChapterBuilder here
|
||||
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))
|
||||
{
|
||||
_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);
|
||||
}
|
||||
else
|
||||
|
@ -680,6 +686,7 @@ public class ProcessSeries : IProcessSeries
|
|||
if (!forceUpdate && !_fileService.HasFileBeenModifiedSince(existingFile.FilePath, existingFile.LastModified) && existingFile.Pages != 0) return;
|
||||
existingFile.Pages = _readingItemService.GetNumberOfPages(info.FullFilePath, info.Format);
|
||||
existingFile.Extension = fileInfo.Extension.ToLowerInvariant();
|
||||
existingFile.FileName = Path.GetFileNameWithoutExtension(existingFile.FilePath);
|
||||
existingFile.Bytes = fileInfo.Length;
|
||||
// We skip updating DB here with last modified time so that metadata refresh can do it
|
||||
}
|
||||
|
|
|
@ -247,11 +247,17 @@ public class Startup
|
|||
|
||||
// v0.7.14
|
||||
await MigrateEmailTemplates.Migrate(directoryService, logger);
|
||||
await MigrateVolumeNumber.Migrate(unitOfWork, dataContext, logger);
|
||||
await MigrateWantToReadImport.Migrate(unitOfWork, directoryService, logger);
|
||||
await MigrateVolumeNumber.Migrate(dataContext, logger);
|
||||
await MigrateWantToReadImport.Migrate(unitOfWork, dataContext, directoryService, logger);
|
||||
await MigrateManualHistory.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
|
||||
var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);
|
||||
installVersion.Value = BuildInfo.Version.ToString();
|
||||
|
|
|
@ -9,12 +9,12 @@ Setup guides, FAQ, the more information we have on the [wiki](https://wiki.kavit
|
|||
|
||||
### 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/).
|
||||
- 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)
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [NodeJS](https://nodejs.org/en/download/) (Node 18.13.X or higher)
|
||||
- .NET 7.0+
|
||||
- dotnet tool install -g --version 6.4.0 Swashbuckle.AspNetCore.Cli
|
||||
- .NET 8.0+
|
||||
- dotnet tool install -g --version 6.5.0 Swashbuckle.AspNetCore.Cli
|
||||
|
||||
### Getting started ###
|
||||
|
||||
|
@ -24,6 +24,7 @@ Setup guides, FAQ, the more information we have on the [wiki](https://wiki.kavit
|
|||
- cd Kavita/UI/Web
|
||||
- `npm install`
|
||||
- `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`
|
||||
5. Build the project in Visual Studio/Rider, Setting startup project to `API`
|
||||
6. Debug the project in Visual Studio/Rider
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"minify-langs": "node minify-json.js",
|
||||
"cache-locale": "node hash-localization.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",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e"
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { MangaFile } from './manga-file';
|
||||
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.
|
||||
|
@ -9,7 +10,12 @@ export const LooseLeafOrSpecialNumber = 0;
|
|||
export interface Chapter {
|
||||
id: number;
|
||||
range: string;
|
||||
/**
|
||||
* @deprecated Use minNumber/maxNumber
|
||||
*/
|
||||
number: string;
|
||||
minNumber: number;
|
||||
maxNumber: number;
|
||||
files: Array<MangaFile>;
|
||||
/**
|
||||
* This is used in the UI, it is not updated or sent to Backend
|
||||
|
|
|
@ -8,7 +8,6 @@ import {TranslocoService} from "@ngneat/transloco";
|
|||
})
|
||||
export class DefaultDatePipe implements PipeTransform {
|
||||
|
||||
// TODO: Figure out how to translate Never
|
||||
constructor(private translocoService: TranslocoService) {
|
||||
}
|
||||
transform(value: any, replacementString = 'default-date-pipe.never'): string {
|
||||
|
|
|
@ -62,7 +62,15 @@
|
|||
<td>
|
||||
<ng-container [ngSwitch]="item.scrobbleEventType">
|
||||
<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 *ngSwitchCase="ScrobbleEventType.ScoreUpdated">
|
||||
{{t('rating', {r: item.rating})}}
|
||||
|
|
|
@ -16,6 +16,7 @@ import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
|
|||
import {TranslocoLocaleModule} from "@ngneat/transloco-locale";
|
||||
import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {LooseLeafOrDefaultNumber, SpecialVolumeNumber} from "../../_models/chapter";
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-scrobble-history',
|
||||
|
@ -101,4 +102,6 @@ export class UserScrobbleHistoryComponent implements OnInit {
|
|||
}
|
||||
|
||||
|
||||
protected readonly SpecialVolumeNumber = SpecialVolumeNumber;
|
||||
protected readonly LooseLeafOrDefaultNumber = LooseLeafOrDefaultNumber;
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ export class ManageLogsComponent implements OnInit, OnDestroy {
|
|||
this.hubConnection.on('SendLogAsObject', resp => {
|
||||
const payload = resp.arguments[0] as LogMessage;
|
||||
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();
|
||||
values.push(logMessage);
|
||||
this.logsSource.next(values);
|
||||
|
@ -60,7 +60,7 @@ export class ManageLogsComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// unsubscrbe from signalr connection
|
||||
// unsubscribe from signalr connection
|
||||
if (this.hubConnection) {
|
||||
this.hubConnection.stop().catch(err => console.error(err));
|
||||
console.log('Stoping log connection');
|
||||
|
|
|
@ -297,6 +297,7 @@ export class EditSeriesModalComponent implements OnInit {
|
|||
});
|
||||
this.seriesVolumes.forEach(vol => {
|
||||
vol.volumeFiles = vol.chapters?.sort(this.utilityService.sortChapters).map((c: Chapter) => c.files.map((f: any) => {
|
||||
// TODO: Identify how to fix this hack
|
||||
f.chapter = c.number;
|
||||
return f;
|
||||
})).flat();
|
||||
|
|
|
@ -123,7 +123,7 @@
|
|||
<span>
|
||||
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions"
|
||||
[labelBy]="utilityService.formatChapterName(libraryType, true, true) + formatChapterNumber(chapter)"></app-card-actionables>
|
||||
<ng-container *ngIf="chapter.number !== '0'; else specialHeader">
|
||||
<ng-container *ngIf="chapter.minNumber !== LooseLeafOrSpecialNumber; else specialHeader">
|
||||
{{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
|
||||
</ng-container>
|
||||
</span>
|
||||
|
|
|
@ -20,7 +20,7 @@ import { ToastrService } from 'ngx-toastr';
|
|||
import { Observable, of, map, shareReplay } from 'rxjs';
|
||||
import { DownloadService } from 'src/app/shared/_services/download.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 { Device } from 'src/app/_models/device/device';
|
||||
import { LibraryType } from 'src/app/_models/library/library';
|
||||
|
@ -74,6 +74,7 @@ export class CardDetailDrawerComponent implements OnInit {
|
|||
protected readonly Breakpoint = Breakpoint;
|
||||
protected readonly LibraryType = LibraryType;
|
||||
protected readonly TabID = TabID;
|
||||
protected readonly LooseLeafOrSpecialNumber = LooseLeafOrDefaultNumber;
|
||||
|
||||
@Input() parentName = '';
|
||||
@Input() seriesId: number = 0;
|
||||
|
@ -182,10 +183,10 @@ export class CardDetailDrawerComponent implements OnInit {
|
|||
}
|
||||
|
||||
formatChapterNumber(chapter: Chapter) {
|
||||
if (chapter.number === '0') {
|
||||
if (chapter.minNumber === LooseLeafOrDefaultNumber) {
|
||||
return '1';
|
||||
}
|
||||
return chapter.number;
|
||||
return chapter.minNumber + '';
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<any>, chapter: Chapter) {
|
||||
|
@ -281,5 +282,4 @@ export class CardDetailDrawerComponent implements OnInit {
|
|||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -204,7 +204,7 @@ export class CardItemComponent implements OnInit {
|
|||
if (volumeTitle === '' || volumeTitle === null || volumeTitle === undefined) {
|
||||
this.tooltipTitle = (this.title).trim();
|
||||
} else {
|
||||
this.tooltipTitle = (this.utilityService.asChapter(this.entity).volumeTitle + ' ' + this.title).trim();
|
||||
this.tooltipTitle = (volumeTitle + ' ' + this.title).trim();
|
||||
}
|
||||
} else {
|
||||
this.tooltipTitle = chapterTitle;
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
<ng-template #fullComicTitle>
|
||||
{{seriesName.length > 0 ? seriesName + ' - ' : ''}}
|
||||
<ng-container *ngIf="includeVolume && volumeTitle !== ''">
|
||||
{{Number !== LooseLeafOrSpecialNumber ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
|
||||
{{Number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
|
||||
</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-container>
|
||||
<ng-container *ngSwitchCase="LibraryType.Manga">
|
||||
|
@ -19,9 +19,9 @@
|
|||
<ng-template #fullMangaTitle>
|
||||
{{seriesName.length > 0 ? seriesName + ' - ' : ''}}
|
||||
<ng-container *ngIf="includeVolume && volumeTitle !== ''">
|
||||
{{Number !== LooseLeafOrSpecialNumber ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
|
||||
{{Number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
|
||||
</ng-container>
|
||||
{{Number !== LooseLeafOrSpecialNumber ? (isChapter ? (t('chapter') + ' ') + Number : volumeTitle) : t('special')}}
|
||||
{{Number !== LooseLeafOrSpecial ? (isChapter ? (t('chapter') + ' ') + Number : volumeTitle) : t('special')}}
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="LibraryType.Book">
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
|
||||
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 { Volume } from 'src/app/_models/volume';
|
||||
import {CommonModule, NgSwitch} from "@angular/common";
|
||||
import {TranslocoModule} from "@ngneat/transloco";
|
||||
|
||||
/**
|
||||
* This is primarily used for list item
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-entity-title',
|
||||
standalone: true,
|
||||
|
@ -20,7 +23,9 @@ import {TranslocoModule} from "@ngneat/transloco";
|
|||
})
|
||||
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
|
||||
|
@ -42,19 +47,18 @@ export class EntityTitleComponent implements OnInit {
|
|||
volumeTitle: string = '';
|
||||
|
||||
get Number() {
|
||||
if (this.utilityService.isVolume(this.entity)) return (this.entity as Volume).minNumber;
|
||||
return (this.entity as Chapter).number;
|
||||
if (this.isChapter) return (this.entity as Chapter).range;
|
||||
return (this.entity as Volume).name;
|
||||
}
|
||||
|
||||
get LibraryType() {
|
||||
return LibraryType;
|
||||
}
|
||||
|
||||
constructor(private utilityService: UtilityService, private readonly cdRef: ChangeDetectorRef) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isChapter = this.utilityService.isChapter(this.entity);
|
||||
|
||||
|
||||
|
||||
if (this.isChapter) {
|
||||
const c = (this.entity as Chapter);
|
||||
this.volumeTitle = c.volumeTitle || '';
|
||||
|
|
|
@ -308,7 +308,7 @@
|
|||
<ng-container *ngIf="nextExpectedChapter">
|
||||
<ng-container [ngSwitch]="tabId">
|
||||
<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"
|
||||
[imageUrl]="imageService.getSeriesCoverImage(series.id)"></app-next-expected-card>
|
||||
</ng-container>
|
||||
|
|
|
@ -55,7 +55,7 @@ import {
|
|||
import {TagBadgeCursor} from 'src/app/shared/tag-badge/tag-badge.component';
|
||||
import {DownloadEvent, DownloadService} from 'src/app/shared/_services/download.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 {ScanSeriesEvent} from 'src/app/_models/events/scan-series-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 {User} from 'src/app/_models/user';
|
||||
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 {Action, ActionFactoryService, ActionItem} from 'src/app/_services/action-factory.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 TabID = TabID;
|
||||
protected readonly TagBadgeCursor = TagBadgeCursor;
|
||||
protected readonly LooseLeafOrSpecialNumber = LooseLeafOrSpecialNumber;
|
||||
protected readonly LooseLeafOrSpecialNumber = LooseLeafOrDefaultNumber;
|
||||
protected readonly SpecialVolumeNumber = SpecialVolumeNumber;
|
||||
|
||||
@ViewChild('scrollingBlock') scrollingBlock: 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
|
||||
*/
|
||||
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}`;
|
||||
trackBySeriesIdentify = (index: number, item: Series) => `${item.name}_${item.libraryId}_${item.pagesRead}`;
|
||||
trackByStoryLineIdentity = (index: number, item: StoryLineItem) => {
|
||||
|
@ -371,13 +372,13 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
|
||||
// This is a lone chapter
|
||||
if (vol.length === 0) {
|
||||
return 'Ch ' + this.currentlyReadingChapter.number;
|
||||
return 'Ch ' + this.currentlyReadingChapter.minNumber; // 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 + ' Ch ' + this.currentlyReadingChapter.number;
|
||||
return 'Vol ' + vol[0].minNumber + ' Ch ' + this.currentlyReadingChapter.minNumber;
|
||||
}
|
||||
|
||||
return this.currentlyReadingChapter.title;
|
||||
|
|
|
@ -119,7 +119,7 @@ export class DownloadService {
|
|||
case 'volume':
|
||||
return (downloadEntity as Volume).minNumber + '';
|
||||
case 'chapter':
|
||||
return (downloadEntity as Chapter).number;
|
||||
return (downloadEntity as Chapter).minNumber + '';
|
||||
case 'bookmark':
|
||||
return '';
|
||||
case 'logs':
|
||||
|
|
|
@ -43,7 +43,7 @@ export class UtilityService {
|
|||
|
||||
|
||||
sortChapters = (a: Chapter, b: Chapter) => {
|
||||
return parseFloat(a.number) - parseFloat(b.number);
|
||||
return a.minNumber - b.minNumber;
|
||||
}
|
||||
|
||||
mangaFormatToText(format: MangaFormat): string {
|
||||
|
|
|
@ -6,5 +6,5 @@ export interface ReadHistoryEvent {
|
|||
libraryId: number;
|
||||
readDate: string;
|
||||
chapterId: number;
|
||||
chapterNumber: string;
|
||||
chapterNumber: number;
|
||||
}
|
|
@ -42,6 +42,8 @@
|
|||
"is-processed-header": "Is Processed",
|
||||
"no-data": "No Data",
|
||||
"volume-and-chapter-num": "Volume {{v}} Chapter {{n}}",
|
||||
"volume-num": "Volume {{num}}",
|
||||
"chapter-num": "Chapter {{num}}",
|
||||
"rating": "Rating {{r}}",
|
||||
"not-applicable": "Not Applicable",
|
||||
"processed": "Processed",
|
||||
|
|
57
openapi.json
57
openapi.json
|
@ -7,7 +7,7 @@
|
|||
"name": "GPL-3.0",
|
||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||
},
|
||||
"version": "0.7.14.2"
|
||||
"version": "0.7.14.3"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
|
@ -13718,13 +13718,29 @@
|
|||
},
|
||||
"range": {
|
||||
"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
|
||||
},
|
||||
"number": {
|
||||
"type": "string",
|
||||
"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": {
|
||||
"type": "array",
|
||||
|
@ -13926,13 +13942,26 @@
|
|||
},
|
||||
"range": {
|
||||
"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
|
||||
},
|
||||
"number": {
|
||||
"type": "string",
|
||||
"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": {
|
||||
"type": "integer",
|
||||
|
@ -16163,6 +16192,11 @@
|
|||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"fileName": {
|
||||
"type": "string",
|
||||
"description": "The filename without extension",
|
||||
"nullable": true
|
||||
},
|
||||
"filePath": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the archive file",
|
||||
|
@ -16691,8 +16725,8 @@
|
|||
"format": "int32"
|
||||
},
|
||||
"chapterNumber": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
@ -20376,6 +20410,11 @@
|
|||
"description": "A String representation of the volume number. Allows for floats. Can also include a range (1-2).",
|
||||
"nullable": true
|
||||
},
|
||||
"lookupName": {
|
||||
"type": "string",
|
||||
"description": "This is just the original Parsed volume number for lookups",
|
||||
"nullable": true
|
||||
},
|
||||
"number": {
|
||||
"type": "integer",
|
||||
"description": "The minimum number in the Name field in Int form",
|
||||
|
@ -20472,9 +20511,9 @@
|
|||
"nullable": true
|
||||
},
|
||||
"number": {
|
||||
"type": "number",
|
||||
"type": "integer",
|
||||
"description": "This will map to MinNumber. Number was removed in v0.7.13.8/v0.7.14",
|
||||
"format": "float",
|
||||
"format": "int32",
|
||||
"deprecated": true
|
||||
},
|
||||
"pages": {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue