Comic Rework, New Scanner, Foundation Overahul (is this a full release?) (#2780)

This commit is contained in:
Joe Milazzo 2024-03-17 12:58:32 -05:00 committed by GitHub
parent d7e9e7c832
commit 7552c3f5fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
182 changed files with 27630 additions and 3046 deletions

View File

@ -6,17 +6,17 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="20.0.15" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="20.0.15" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="20.0.28" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="20.0.28" />
<PackageReference Include="xunit" Version="2.7.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>

View File

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

View File

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

View File

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

View File

@ -105,6 +105,32 @@ public class ChapterListExtensionsTests
Assert.Equal(chapterList[0], actualChapter);
}
[Fact]
public void GetChapterByRange_On_FilenameChange_ShouldGetChapter()
{
var info = new ParserInfo()
{
Chapters = "1",
Edition = "",
Format = MangaFormat.Archive,
FullFilePath = "/manga/detective comics #001.cbz",
Filename = "detective comics #001.cbz",
IsSpecial = false,
Series = "detective comics",
Title = "detective comics",
Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume
};
var chapterList = new List<Chapter>()
{
CreateChapter("1", "1", CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), false),
};
var actualChapter = chapterList.GetChapterByRange(info);
Assert.Equal(chapterList[0], actualChapter);
}
#region GetFirstChapterWithFiles
[Fact]

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions.TestingHelpers;
using System.Linq;
using API.Entities.Enums;
@ -18,9 +19,8 @@ public class ParserInfoListExtensions
private readonly IDefaultParser _defaultParser;
public ParserInfoListExtensions()
{
_defaultParser =
new DefaultParser(new DirectoryService(Substitute.For<ILogger<DirectoryService>>(),
new MockFileSystem()));
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem());
_defaultParser = new BasicParser(ds, new ImageParser(ds));
}
[Theory]
@ -33,7 +33,7 @@ public class ParserInfoListExtensions
[Theory]
[InlineData(new[] {@"Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip"}, new[] {@"E:\Manga\Cynthia the Mission\Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip"}, true)]
[InlineData(new[] {@"Cynthia The Mission - c000-006 (v06-07) [Desudesu&Brolen].zip"}, new[] {@"E:\Manga\Cynthia the Mission\Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip"}, true)]
[InlineData(new[] {@"Cynthia The Mission - c000-006 (v06-07) [Desudesu&Brolen].zip"}, new[] {@"E:\Manga\Cynthia the Mission\Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip"}, false)]
[InlineData(new[] {@"Cynthia The Mission v20 c12-20 [Desudesu&Brolen].zip"}, new[] {@"E:\Manga\Cynthia the Mission\Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip"}, false)]
public void HasInfoTest(string[] inputInfos, string[] inputChapters, bool expectedHasInfo)
{
@ -41,8 +41,8 @@ public class ParserInfoListExtensions
foreach (var filename in inputInfos)
{
infos.Add(_defaultParser.Parse(
filename,
string.Empty));
Path.Join("E:/Manga/Cynthia the Mission/", filename),
"E:/Manga/", "E:/Manga/", LibraryType.Manga));
}
var files = inputChapters.Select(s => new MangaFileBuilder(s, MangaFormat.Archive, 199).Build()).ToList();
@ -52,4 +52,26 @@ public class ParserInfoListExtensions
Assert.Equal(expectedHasInfo, infos.HasInfo(chapter));
}
[Fact]
public void HasInfoTest_SuccessWhenSpecial()
{
var infos = new[]
{
_defaultParser.Parse(
"E:/Manga/Cynthia the Mission/Cynthia The Mission The Special SP01 [Desudesu&Brolen].zip",
"E:/Manga/", "E:/Manga/", LibraryType.Manga)
};
var files = new[] {@"E:\Manga\Cynthia the Mission\Cynthia The Mission The Special SP01 [Desudesu&Brolen].zip"}
.Select(s => new MangaFileBuilder(s, MangaFormat.Archive, 199).Build())
.ToList();
var chapter = new ChapterBuilder("Cynthia The Mission The Special SP01 [Desudesu&Brolen].zip")
.WithRange("Cynthia The Mission The Special SP01 [Desudesu&Brolen]")
.WithFiles(files)
.WithIsSpecial(true)
.Build();
Assert.True(infos.HasInfo(chapter));
}
}

View File

@ -1,11 +1,9 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Linq;
using API.Comparators;
using API.Entities;
using API.Entities.Enums;
using API.Extensions;
using API.Helpers.Builders;
using API.Services.Tasks.Scanner.Parser;
using Xunit;
namespace API.Tests.Extensions;
@ -17,22 +15,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)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithVolume(new VolumeBuilder(Parser.SpecialVolume)
.WithChapter(new ChapterBuilder(Parser.DefaultChapter)
.WithCoverImage("Special 1")
.WithIsSpecial(true)
.WithSortOrder(Parser.SpecialVolumeNumber + 1)
.Build())
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithChapter(new ChapterBuilder(Parser.DefaultChapter)
.WithCoverImage("Special 2")
.WithIsSpecial(true)
.WithSortOrder(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());
@ -43,8 +42,8 @@ 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(Parser.LooseLeafVolume)
.WithName(Parser.LooseLeafVolume)
.WithChapter(new ChapterBuilder("13")
.WithCoverImage("Chapter 13")
.Build())
@ -59,7 +58,7 @@ public class SeriesExtensionsTests
.WithVolume(new VolumeBuilder("2")
.WithName("Volume 2")
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithChapter(new ChapterBuilder(Parser.DefaultChapter)
.WithCoverImage("Volume 2")
.Build())
.Build())
@ -67,12 +66,83 @@ 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(Parser.LooseLeafVolume)
.WithName(Parser.LooseLeafVolume)
.WithChapter(new ChapterBuilder("-1")
.WithCoverImage("Chapter -1")
.Build())
.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())
.WithChapter(new ChapterBuilder("3")
.WithCoverImage("Chapter 3")
.Build())
.WithChapter(new ChapterBuilder("4AU")
.WithCoverImage("Chapter 4AU")
.Build())
.Build())
.Build();
Assert.Equal("Chapter 1", series.GetCoverImage());
}
/// <summary>
/// Checks the case where there are specials and loose leafs, loose leaf chapters should be preferred
/// </summary>
[Fact]
public void GetCoverImage_LooseChapters_WithSub1_Chapter_WithSpecials()
{
var series = new SeriesBuilder("Test 1")
.WithFormat(MangaFormat.Archive)
.WithVolume(new VolumeBuilder(Parser.SpecialVolume)
.WithName(Parser.SpecialVolume)
.WithChapter(new ChapterBuilder("I am a Special")
.WithCoverImage("I am a Special")
.Build())
.WithChapter(new ChapterBuilder("I am a Special 2")
.WithCoverImage("I am a Special 2")
.Build())
.Build())
.WithVolume(new VolumeBuilder(Parser.LooseLeafVolume)
.WithName(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()
{
@ -81,14 +151,14 @@ public class SeriesExtensionsTests
.WithVolume(new VolumeBuilder("1")
.WithName("Volume 1")
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithChapter(new ChapterBuilder(Parser.DefaultChapter)
.WithCoverImage("Volume 1 Chapter 1")
.Build())
.Build())
.WithVolume(new VolumeBuilder("2")
.WithName("Volume 2")
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithChapter(new ChapterBuilder(Parser.DefaultChapter)
.WithCoverImage("Volume 2")
.Build())
.Build())
@ -109,7 +179,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());
@ -120,8 +190,8 @@ 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(Parser.LooseLeafVolume)
.WithName(Parser.LooseLeafVolume)
.WithChapter(new ChapterBuilder("2.5")
.WithIsSpecial(false)
.WithCoverImage("Special 1")
@ -135,7 +205,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());
@ -146,8 +216,8 @@ 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(Parser.LooseLeafVolume)
.WithName(Parser.LooseLeafVolume)
.WithChapter(new ChapterBuilder("2.5")
.WithIsSpecial(false)
.WithCoverImage("Chapter 2.5")
@ -156,16 +226,19 @@ public class SeriesExtensionsTests
.WithIsSpecial(false)
.WithCoverImage("Chapter 2")
.Build())
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.Build())
.WithVolume(new VolumeBuilder(Parser.SpecialVolume)
.WithChapter(new ChapterBuilder(Parser.DefaultChapter)
.WithIsSpecial(true)
.WithCoverImage("Special 1")
.WithSortOrder(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());
@ -176,8 +249,8 @@ 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(Parser.LooseLeafVolume)
.WithName(Parser.LooseLeafVolume)
.WithChapter(new ChapterBuilder("2.5")
.WithIsSpecial(false)
.WithCoverImage("Chapter 2.5")
@ -186,14 +259,17 @@ public class SeriesExtensionsTests
.WithIsSpecial(false)
.WithCoverImage("Chapter 2")
.Build())
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.Build())
.WithVolume(new VolumeBuilder(Parser.SpecialVolume)
.WithChapter(new ChapterBuilder(Parser.DefaultChapter)
.WithIsSpecial(true)
.WithCoverImage("Special 3")
.WithSortOrder(Parser.SpecialVolumeNumber + 1)
.Build())
.Build())
.WithVolume(new VolumeBuilder("1")
.WithMinNumber(1)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithChapter(new ChapterBuilder(Parser.DefaultChapter)
.WithIsSpecial(false)
.WithCoverImage("Volume 1")
.Build())
@ -202,7 +278,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());
@ -213,8 +289,8 @@ 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(Parser.LooseLeafVolume)
.WithName(Parser.LooseLeafVolume)
.WithChapter(new ChapterBuilder("2.5")
.WithIsSpecial(false)
.WithCoverImage("Chapter 2.5")
@ -223,14 +299,17 @@ public class SeriesExtensionsTests
.WithIsSpecial(false)
.WithCoverImage("Chapter 2")
.Build())
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.Build())
.WithVolume(new VolumeBuilder(Parser.SpecialVolume)
.WithChapter(new ChapterBuilder(Parser.DefaultChapter)
.WithIsSpecial(true)
.WithCoverImage("Special 1")
.WithSortOrder(Parser.SpecialVolumeNumber + 1)
.Build())
.Build())
.WithVolume(new VolumeBuilder("1")
.WithMinNumber(1)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithChapter(new ChapterBuilder(Parser.DefaultChapter)
.WithIsSpecial(false)
.WithCoverImage("Volume 1")
.Build())
@ -239,7 +318,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());
@ -250,8 +329,8 @@ public class SeriesExtensionsTests
{
var series = new SeriesBuilder("Ippo")
.WithFormat(MangaFormat.Archive)
.WithVolume(new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.WithName(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)
.WithVolume(new VolumeBuilder(Parser.LooseLeafVolume)
.WithName(Parser.LooseLeafVolume)
.WithChapter(new ChapterBuilder("1426")
.WithIsSpecial(false)
.WithCoverImage("Chapter 1426")
@ -260,21 +339,24 @@ public class SeriesExtensionsTests
.WithIsSpecial(false)
.WithCoverImage("Chapter 1425")
.Build())
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.Build())
.WithVolume(new VolumeBuilder(Parser.SpecialVolume)
.WithChapter(new ChapterBuilder(Parser.DefaultChapter)
.WithIsSpecial(true)
.WithCoverImage("Special 1")
.WithCoverImage("Special 3")
.WithSortOrder(Parser.SpecialVolumeNumber + 1)
.Build())
.Build())
.WithVolume(new VolumeBuilder("1")
.WithMinNumber(1)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithChapter(new ChapterBuilder(Parser.DefaultChapter)
.WithIsSpecial(false)
.WithCoverImage("Volume 1")
.Build())
.Build())
.WithVolume(new VolumeBuilder("137")
.WithMinNumber(1)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithChapter(new ChapterBuilder(Parser.DefaultChapter)
.WithIsSpecial(false)
.WithCoverImage("Volume 137")
.Build())
@ -283,7 +365,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());
@ -294,8 +376,8 @@ 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(Parser.LooseLeafVolume)
.WithName(Parser.LooseLeafVolume)
.WithChapter(new ChapterBuilder("2.5")
.WithIsSpecial(false)
.WithCoverImage("Chapter 2.5")
@ -307,7 +389,7 @@ public class SeriesExtensionsTests
.Build())
.WithVolume(new VolumeBuilder("4")
.WithMinNumber(4)
.WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)
.WithChapter(new ChapterBuilder(Parser.DefaultChapter)
.WithIsSpecial(false)
.WithCoverImage("Volume 4")
.Build())
@ -316,11 +398,77 @@ 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());
}
/// <summary>
/// Ensure that Series cover is issue 1, when there are less than 1 entities and specials
/// </summary>
[Fact]
public void GetCoverImage_LessThanIssue1()
{
var series = new SeriesBuilder("Test 1")
.WithFormat(MangaFormat.Archive)
.WithVolume(new VolumeBuilder(Parser.LooseLeafVolume)
.WithName(Parser.LooseLeafVolume)
.WithChapter(new ChapterBuilder("0")
.WithIsSpecial(false)
.WithCoverImage("Chapter 0")
.Build())
.WithChapter(new ChapterBuilder("1")
.WithIsSpecial(false)
.WithCoverImage("Chapter 1")
.Build())
.Build())
.WithVolume(new VolumeBuilder(Parser.SpecialVolume)
.WithMinNumber(4)
.WithChapter(new ChapterBuilder(Parser.DefaultChapter)
.WithIsSpecial(false)
.WithCoverImage("Volume 4")
.Build())
.Build())
.Build();
Assert.Equal("Chapter 1", series.GetCoverImage());
}
/// <summary>
/// Ensure that Series cover is issue 1, when there are less than 1 entities and specials
/// </summary>
[Fact]
public void GetCoverImage_LessThanIssue1_WithNegative()
{
var series = new SeriesBuilder("Test 1")
.WithFormat(MangaFormat.Archive)
.WithVolume(new VolumeBuilder(Parser.LooseLeafVolume)
.WithName(Parser.LooseLeafVolume)
.WithChapter(new ChapterBuilder("-1")
.WithIsSpecial(false)
.WithCoverImage("Chapter -1")
.Build())
.WithChapter(new ChapterBuilder("0")
.WithIsSpecial(false)
.WithCoverImage("Chapter 0")
.Build())
.WithChapter(new ChapterBuilder("1")
.WithIsSpecial(false)
.WithCoverImage("Chapter 1")
.Build())
.Build())
.WithVolume(new VolumeBuilder(Parser.SpecialVolume)
.WithMinNumber(4)
.WithChapter(new ChapterBuilder(Parser.DefaultChapter)
.WithIsSpecial(false)
.WithCoverImage("Volume 4")
.Build())
.Build())
.Build();
Assert.Equal("Chapter 1", series.GetCoverImage());
}
}

View File

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

View File

@ -1,6 +1,7 @@
using System.Collections.Generic;
using API.Data;
using API.Entities;
using API.Extensions;
using API.Helpers;
using API.Helpers.Builders;
using Xunit;
@ -12,42 +13,51 @@ public class GenreHelperTests
[Fact]
public void UpdateGenre_ShouldAddNewGenre()
{
var allGenres = new List<Genre>
var allGenres = new Dictionary<string, Genre>
{
new GenreBuilder("Action").Build(),
new GenreBuilder("action").Build(),
new GenreBuilder("Sci-fi").Build(),
{"Action".ToNormalized(), new GenreBuilder("Action").Build()},
{"Sci-fi".ToNormalized(), new GenreBuilder("Sci-fi").Build()}
};
var genreAdded = new List<Genre>();
var addedCount = 0;
GenreHelper.UpdateGenre(allGenres, new[] {"Action", "Adventure"}, genre =>
GenreHelper.UpdateGenre(allGenres, new[] {"Action", "Adventure"}, (genre, isNew) =>
{
if (isNew)
{
addedCount++;
}
genreAdded.Add(genre);
});
Assert.Equal(2, genreAdded.Count);
Assert.Equal(4, allGenres.Count);
Assert.Equal(1, addedCount);
Assert.Equal(3, allGenres.Count);
}
[Fact]
public void UpdateGenre_ShouldNotAddDuplicateGenre()
{
var allGenres = new List<Genre>
var allGenres = new Dictionary<string, Genre>
{
new GenreBuilder("Action").Build(),
new GenreBuilder("action").Build(),
new GenreBuilder("Sci-fi").Build(),
{"Action".ToNormalized(), new GenreBuilder("Action").Build()},
{"Sci-fi".ToNormalized(), new GenreBuilder("Sci-fi").Build()}
};
var genreAdded = new List<Genre>();
var addedCount = 0;
GenreHelper.UpdateGenre(allGenres, new[] {"Action", "Scifi"}, genre =>
GenreHelper.UpdateGenre(allGenres, new[] {"Action", "Scifi"}, (genre, isNew) =>
{
if (isNew)
{
addedCount++;
}
genreAdded.Add(genre);
});
Assert.Equal(3, allGenres.Count);
Assert.Equal(0, addedCount);
Assert.Equal(2, genreAdded.Count);
Assert.Equal(2, allGenres.Count);
}
[Fact]

View File

@ -1,6 +1,8 @@
using System.Collections.Generic;
using System.Linq;
using API.Data;
using API.Entities;
using API.Extensions;
using API.Helpers;
using API.Helpers.Builders;
using Xunit;
@ -12,50 +14,50 @@ public class TagHelperTests
[Fact]
public void UpdateTag_ShouldAddNewTag()
{
var allTags = new List<Tag>
var allTags = new Dictionary<string, Tag>
{
new TagBuilder("Action").Build(),
new TagBuilder("action").Build(),
new TagBuilder("Sci-fi").Build(),
{"Action".ToNormalized(), new TagBuilder("Action").Build()},
{"Sci-fi".ToNormalized(), new TagBuilder("Sci-fi").Build()}
};
var tagAdded = new List<Tag>();
var tagCalled = new List<Tag>();
var addedCount = 0;
TagHelper.UpdateTag(allTags, new[] {"Action", "Adventure"}, (tag, added) =>
{
if (added)
{
tagAdded.Add(tag);
addedCount++;
}
tagCalled.Add(tag);
});
Assert.Single(tagAdded);
Assert.Equal(4, allTags.Count);
Assert.Equal(1, addedCount);
Assert.Equal(2, tagCalled.Count());
Assert.Equal(3, allTags.Count);
}
[Fact]
public void UpdateTag_ShouldNotAddDuplicateTag()
{
var allTags = new List<Tag>
var allTags = new Dictionary<string, Tag>
{
new TagBuilder("Action").Build(),
new TagBuilder("action").Build(),
new TagBuilder("Sci-fi").Build(),
{"Action".ToNormalized(), new TagBuilder("Action").Build()},
{"Sci-fi".ToNormalized(), new TagBuilder("Sci-fi").Build()}
};
var tagAdded = new List<Tag>();
var tagCalled = new List<Tag>();
var addedCount = 0;
TagHelper.UpdateTag(allTags, new[] {"Action", "Scifi"}, (tag, added) =>
{
if (added)
{
tagAdded.Add(tag);
addedCount++;
}
TagHelper.AddTagIfNotExists(allTags, tag);
tagCalled.Add(tag);
});
Assert.Equal(3, allTags.Count);
Assert.Empty(tagAdded);
Assert.Equal(2, allTags.Count);
Assert.Equal(0, addedCount);
}
[Fact]

View File

@ -0,0 +1,217 @@
using System.IO.Abstractions.TestingHelpers;
using API.Entities.Enums;
using API.Services;
using API.Services.Tasks.Scanner.Parser;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
namespace API.Tests.Parsers;
public class BasicParserTests
{
private readonly BasicParser _parser;
private readonly ILogger<DirectoryService> _dsLogger = Substitute.For<ILogger<DirectoryService>>();
private const string RootDirectory = "C:/Books/";
public BasicParserTests()
{
var fileSystem = new MockFileSystem();
fileSystem.AddDirectory("C:/Books/");
fileSystem.AddFile("C:/Books/Harry Potter/Harry Potter - Vol 1.epub", new MockFileData(""));
fileSystem.AddFile("C:/Books/Accel World/Accel World - Volume 1.cbz", new MockFileData(""));
fileSystem.AddFile("C:/Books/Accel World/Accel World - Volume 1 Chapter 2.cbz", new MockFileData(""));
fileSystem.AddFile("C:/Books/Accel World/Accel World - Chapter 3.cbz", new MockFileData(""));
fileSystem.AddFile("C:/Books/Accel World/Accel World Gaiden SP01.cbz", new MockFileData(""));
fileSystem.AddFile("C:/Books/Accel World/cover.png", new MockFileData(""));
fileSystem.AddFile("C:/Books/Batman/Batman #1.cbz", new MockFileData(""));
var ds = new DirectoryService(_dsLogger, fileSystem);
_parser = new BasicParser(ds, new ImageParser(ds));
}
#region Parse_Books
#endregion
#region Parse_Manga
/// <summary>
/// Tests that when there is a loose leaf cover in the manga library, that it is ignored
/// </summary>
[Fact]
public void Parse_MangaLibrary_JustCover_ShouldReturnNull()
{
var actual = _parser.Parse(@"C:/Books/Accel World/cover.png", "C:/Books/Accel World/",
RootDirectory, LibraryType.Manga, null);
Assert.Null(actual);
}
/// <summary>
/// Tests that when there is a loose leaf cover in the manga library, that it is ignored
/// </summary>
[Fact]
public void Parse_MangaLibrary_OtherImage_ShouldReturnNull()
{
var actual = _parser.Parse(@"C:/Books/Accel World/page 01.png", "C:/Books/Accel World/",
RootDirectory, LibraryType.Manga, null);
Assert.NotNull(actual);
}
/// <summary>
/// Tests that when there is a volume and chapter in filename, it appropriately parses
/// </summary>
[Fact]
public void Parse_MangaLibrary_VolumeAndChapterInFilename()
{
var actual = _parser.Parse("C:/Books/Mujaki no Rakuen/Mujaki no Rakuen Vol12 ch76.cbz", "C:/Books/Mujaki no Rakuen/",
RootDirectory, LibraryType.Manga, null);
Assert.NotNull(actual);
Assert.Equal("Mujaki no Rakuen", actual.Series);
Assert.Equal("12", actual.Volumes);
Assert.Equal("76", actual.Chapters);
Assert.False(actual.IsSpecial);
}
/// <summary>
/// Tests that when there is a volume in filename, it appropriately parses
/// </summary>
[Fact]
public void Parse_MangaLibrary_JustVolumeInFilename()
{
var actual = _parser.Parse("C:/Books/Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen/Vol 1.cbz",
"C:/Books/Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen/",
RootDirectory, LibraryType.Manga, null);
Assert.NotNull(actual);
Assert.Equal("Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen", actual.Series);
Assert.Equal("1", actual.Volumes);
Assert.Equal(Parser.DefaultChapter, actual.Chapters);
Assert.False(actual.IsSpecial);
}
/// <summary>
/// Tests that when there is a chapter only in filename, it appropriately parses
/// </summary>
[Fact]
public void Parse_MangaLibrary_JustChapterInFilename()
{
var actual = _parser.Parse("C:/Books/Beelzebub/Beelzebub_01_[Noodles].zip",
"C:/Books/Beelzebub/",
RootDirectory, LibraryType.Manga, null);
Assert.NotNull(actual);
Assert.Equal("Beelzebub", actual.Series);
Assert.Equal(Parser.LooseLeafVolume, actual.Volumes);
Assert.Equal("1", actual.Chapters);
Assert.False(actual.IsSpecial);
}
/// <summary>
/// Tests that when there is a SP Marker in filename, it appropriately parses
/// </summary>
[Fact]
public void Parse_MangaLibrary_SpecialMarkerInFilename()
{
var actual = _parser.Parse("C:/Books/Summer Time Rendering/Specials/Record 014 (between chapter 083 and ch084) SP11.cbr",
"C:/Books/Summer Time Rendering/",
RootDirectory, LibraryType.Manga, null);
Assert.NotNull(actual);
Assert.Equal("Summer Time Rendering", actual.Series);
Assert.Equal(Parser.SpecialVolume, actual.Volumes);
Assert.Equal(Parser.DefaultChapter, actual.Chapters);
Assert.True(actual.IsSpecial);
}
/// <summary>
/// Tests that when the filename parses as a speical, it appropriately parses
/// </summary>
[Fact]
public void Parse_MangaLibrary_SpecialInFilename()
{
var actual = _parser.Parse("C:/Books/Summer Time Rendering/Specials/Volume Omake.cbr",
"C:/Books/Summer Time Rendering/",
RootDirectory, LibraryType.Manga, null);
Assert.NotNull(actual);
Assert.Equal("Summer Time Rendering", actual.Series);
Assert.Equal("Volume Omake", actual.Title);
Assert.Equal(Parser.SpecialVolume, actual.Volumes);
Assert.Equal(Parser.DefaultChapter, actual.Chapters);
Assert.True(actual.IsSpecial);
}
/// <summary>
/// Tests that when there is an edition in filename, it appropriately parses
/// </summary>
[Fact]
public void Parse_MangaLibrary_EditionInFilename()
{
var actual = _parser.Parse("C:/Books/Air Gear/Air Gear Omnibus v01 (2016) (Digital) (Shadowcat-Empire).cbz",
"C:/Books/Air Gear/",
RootDirectory, LibraryType.Manga, null);
Assert.NotNull(actual);
Assert.Equal("Air Gear", actual.Series);
Assert.Equal("1", actual.Volumes);
Assert.Equal(Parser.DefaultChapter, actual.Chapters);
Assert.False(actual.IsSpecial);
Assert.Equal("Omnibus", actual.Edition);
}
#endregion
#region Parse_Books
/// <summary>
/// Tests that when there is a volume in filename, it appropriately parses
/// </summary>
[Fact]
public void Parse_MangaBooks_JustVolumeInFilename()
{
var actual = _parser.Parse("C:/Books/Epubs/Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub",
"C:/Books/Epubs/",
RootDirectory, LibraryType.Manga, null);
Assert.NotNull(actual);
Assert.Equal("Harrison, Kim - The Good, The Bad, and the Undead - Hollows", actual.Series);
Assert.Equal("2.5", actual.Volumes);
Assert.Equal(Parser.DefaultChapter, actual.Chapters);
}
#endregion
#region IsApplicable
/// <summary>
/// Tests that this Parser can only be used on images and Image library type
/// </summary>
[Fact]
public void IsApplicable_Fails_WhenNonMatchingLibraryType()
{
Assert.False(_parser.IsApplicable("something.cbz", LibraryType.Image));
Assert.False(_parser.IsApplicable("something.cbz", LibraryType.ComicVine));
}
/// <summary>
/// Tests that this Parser can only be used on images and Image library type
/// </summary>
[Fact]
public void IsApplicable_Success_WhenMatchingLibraryType()
{
Assert.True(_parser.IsApplicable("something.png", LibraryType.Manga));
Assert.True(_parser.IsApplicable("something.png", LibraryType.Comic));
Assert.True(_parser.IsApplicable("something.pdf", LibraryType.Book));
Assert.True(_parser.IsApplicable("something.epub", LibraryType.LightNovel));
}
#endregion
}

View File

@ -0,0 +1,74 @@
using System.IO.Abstractions.TestingHelpers;
using API.Data.Metadata;
using API.Entities.Enums;
using API.Services;
using API.Services.Tasks.Scanner.Parser;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
namespace API.Tests.Parsers;
public class BookParserTests
{
private readonly BookParser _parser;
private readonly ILogger<DirectoryService> _dsLogger = Substitute.For<ILogger<DirectoryService>>();
private const string RootDirectory = "C:/Books/";
public BookParserTests()
{
var fileSystem = new MockFileSystem();
fileSystem.AddDirectory("C:/Books/");
fileSystem.AddFile("C:/Books/Harry Potter/Harry Potter - Vol 1.epub", new MockFileData(""));
fileSystem.AddFile("C:/Books/Adam Freeman - Pro ASP.NET Core 6.epub", new MockFileData(""));
fileSystem.AddFile("C:/Books/My Fav Book SP01.epub", new MockFileData(""));
var ds = new DirectoryService(_dsLogger, fileSystem);
_parser = new BookParser(ds, Substitute.For<IBookService>(), new BasicParser(ds, new ImageParser(ds)));
}
#region Parse
// TODO: I'm not sure how to actually test this as it relies on an epub parser to actually do anything
/// <summary>
/// Tests that if there is a Series Folder then Chapter folder, the code appropriately identifies the Series name and Chapter
/// </summary>
// [Fact]
// public void Parse_SeriesWithDirectoryName()
// {
// var actual = _parser.Parse("C:/Books/Harry Potter/Harry Potter - Vol 1.epub", "C:/Books/Birds of Prey/",
// RootDirectory, LibraryType.Book, new ComicInfo()
// {
// Series = "Harry Potter",
// Volume = "1"
// });
//
// Assert.NotNull(actual);
// Assert.Equal("Harry Potter", actual.Series);
// Assert.Equal("1", actual.Volumes);
// }
#endregion
#region IsApplicable
/// <summary>
/// Tests that this Parser can only be used on images and Image library type
/// </summary>
[Fact]
public void IsApplicable_Fails_WhenNonMatchingLibraryType()
{
Assert.False(_parser.IsApplicable("something.cbz", LibraryType.Manga));
Assert.False(_parser.IsApplicable("something.cbz", LibraryType.Book));
}
/// <summary>
/// Tests that this Parser can only be used on images and Image library type
/// </summary>
[Fact]
public void IsApplicable_Success_WhenMatchingLibraryType()
{
Assert.True(_parser.IsApplicable("something.epub", LibraryType.Image));
}
#endregion
}

View File

@ -0,0 +1,115 @@
using System.IO.Abstractions.TestingHelpers;
using API.Data.Metadata;
using API.Entities.Enums;
using API.Services;
using API.Services.Tasks.Scanner.Parser;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
namespace API.Tests.Parsers;
public class ComicVineParserTests
{
private readonly ComicVineParser _parser;
private readonly ILogger<DirectoryService> _dsLogger = Substitute.For<ILogger<DirectoryService>>();
private const string RootDirectory = "C:/Comics/";
public ComicVineParserTests()
{
var fileSystem = new MockFileSystem();
fileSystem.AddDirectory("C:/Comics/");
fileSystem.AddDirectory("C:/Comics/Birds of Prey (2002)");
fileSystem.AddFile("C:/Comics/Birds of Prey (2002)/Birds of Prey 001 (2002).cbz", new MockFileData(""));
fileSystem.AddFile("C:/Comics/DC Comics/Birds of Prey (1999)/Birds of Prey 001 (1999).cbz", new MockFileData(""));
fileSystem.AddFile("C:/Comics/DC Comics/Blood Syndicate/Blood Syndicate 001 (1999).cbz", new MockFileData(""));
var ds = new DirectoryService(_dsLogger, fileSystem);
_parser = new ComicVineParser(ds);
}
#region Parse
/// <summary>
/// Tests that when Series and Volume are filled out, Kavita uses that for the Series Name
/// </summary>
[Fact]
public void Parse_SeriesWithComicInfo()
{
var actual = _parser.Parse("C:/Comics/Birds of Prey (2002)/Birds of Prey 001 (2002).cbz", "C:/Comics/Birds of Prey (2002)/",
RootDirectory, LibraryType.ComicVine, new ComicInfo()
{
Series = "Birds of Prey",
Volume = "2002"
});
Assert.NotNull(actual);
Assert.Equal("Birds of Prey (2002)", actual.Series);
Assert.Equal("2002", actual.Volumes);
}
/// <summary>
/// Tests that no ComicInfo, take the Directory Name if it matches "Series (2002)" or "Series (2)"
/// </summary>
[Fact]
public void Parse_SeriesWithDirectoryNameAsSeriesYear()
{
var actual = _parser.Parse("C:/Comics/Birds of Prey (2002)/Birds of Prey 001 (2002).cbz", "C:/Comics/Birds of Prey (2002)/",
RootDirectory, LibraryType.ComicVine, null);
Assert.NotNull(actual);
Assert.Equal("Birds of Prey (2002)", actual.Series);
Assert.Equal("2002", actual.Volumes);
Assert.Equal("1", actual.Chapters);
}
/// <summary>
/// Tests that no ComicInfo, take a directory name up to root if it matches "Series (2002)" or "Series (2)"
/// </summary>
[Fact]
public void Parse_SeriesWithADirectoryNameAsSeriesYear()
{
var actual = _parser.Parse("C:/Comics/DC Comics/Birds of Prey (1999)/Birds of Prey 001 (1999).cbz", "C:/Comics/DC Comics/",
RootDirectory, LibraryType.ComicVine, null);
Assert.NotNull(actual);
Assert.Equal("Birds of Prey (1999)", actual.Series);
Assert.Equal("1999", actual.Volumes);
Assert.Equal("1", actual.Chapters);
}
/// <summary>
/// Tests that no ComicInfo and nothing matches Series (Volume), then just take the directory name as the Series
/// </summary>
[Fact]
public void Parse_FallbackToDirectoryNameOnly()
{
var actual = _parser.Parse("C:/Comics/DC Comics/Blood Syndicate/Blood Syndicate 001 (1999).cbz", "C:/Comics/DC Comics/",
RootDirectory, LibraryType.ComicVine, null);
Assert.NotNull(actual);
Assert.Equal("Blood Syndicate", actual.Series);
Assert.Equal(Parser.LooseLeafVolume, actual.Volumes);
Assert.Equal("1", actual.Chapters);
}
#endregion
#region IsApplicable
/// <summary>
/// Tests that this Parser can only be used on ComicVine type
/// </summary>
[Fact]
public void IsApplicable_Fails_WhenNonMatchingLibraryType()
{
Assert.False(_parser.IsApplicable("", LibraryType.Comic));
}
/// <summary>
/// Tests that this Parser can only be used on ComicVine type
/// </summary>
[Fact]
public void IsApplicable_Success_WhenMatchingLibraryType()
{
Assert.True(_parser.IsApplicable("", LibraryType.ComicVine));
}
#endregion
}

View File

@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.IO.Abstractions.TestingHelpers;
using System.Linq;
using API.Entities.Enums;
using API.Services;
using API.Services.Tasks.Scanner.Parser;
@ -9,9 +7,8 @@ using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
using Xunit.Abstractions;
using API.Services.Tasks.Scanner.Parser;
namespace API.Tests.Parser;
namespace API.Tests.Parsers;
public class DefaultParserTests
{
@ -22,10 +19,12 @@ public class DefaultParserTests
{
_testOutputHelper = testOutputHelper;
var directoryService = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem());
_defaultParser = new DefaultParser(directoryService);
_defaultParser = new BasicParser(directoryService, new ImageParser(directoryService));
}
#region ParseFromFallbackFolders
[Theory]
[InlineData("C:/", "C:/Love Hina/Love Hina - Special.cbz", "Love Hina")]
@ -34,7 +33,7 @@ public class DefaultParserTests
[InlineData("C:/", "C:/Something Random/Mujaki no Rakuen SP01.cbz", "Something Random")]
public void ParseFromFallbackFolders_FallbackShouldParseSeries(string rootDir, string inputPath, string expectedSeries)
{
var actual = _defaultParser.Parse(inputPath, rootDir);
var actual = _defaultParser.Parse(inputPath, rootDir, rootDir, LibraryType.Manga, null);
if (actual == null)
{
Assert.NotNull(actual);
@ -52,7 +51,7 @@ public class DefaultParserTests
public void ParseFromFallbackFolders_ShouldParseSeriesVolumeAndChapter(string inputFile, string[] expectedParseInfo)
{
const string rootDirectory = "/manga/";
var actual = new ParserInfo {Series = "", Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume};
var actual = new ParserInfo {Series = "", Chapters = Parser.DefaultChapter, Volumes = Parser.LooseLeafVolume};
_defaultParser.ParseFromFallbackFolders(inputFile, rootDirectory, LibraryType.Manga, ref actual);
Assert.Equal(expectedParseInfo[0], actual.Series);
Assert.Equal(expectedParseInfo[1], actual.Volumes);
@ -74,8 +73,8 @@ public class DefaultParserTests
fs.AddDirectory(rootDirectory);
fs.AddFile(inputFile, new MockFileData(""));
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fs);
var parser = new DefaultParser(ds);
var actual = parser.Parse(inputFile, rootDirectory);
var parser = new BasicParser(ds, new ImageParser(ds));
var actual = parser.Parse(inputFile, rootDirectory, rootDirectory, LibraryType.Manga, null);
_defaultParser.ParseFromFallbackFolders(inputFile, rootDirectory, LibraryType.Manga, ref actual);
Assert.Equal(expectedParseInfo, actual.Series);
}
@ -90,8 +89,8 @@ public class DefaultParserTests
fs.AddDirectory(rootDirectory);
fs.AddFile(inputFile, new MockFileData(""));
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fs);
var parser = new DefaultParser(ds);
var actual = parser.Parse(inputFile, rootDirectory);
var parser = new BasicParser(ds, new ImageParser(ds));
var actual = parser.Parse(inputFile, rootDirectory, rootDirectory, LibraryType.Manga, null);
_defaultParser.ParseFromFallbackFolders(inputFile, rootDirectory, LibraryType.Manga, ref actual);
Assert.Equal(expectedParseInfo, actual.Series);
}
@ -101,13 +100,6 @@ public class DefaultParserTests
#region Parse
[Fact]
public void Parse_MangaLibrary_JustCover_ShouldReturnNull()
{
const string rootPath = @"E:/Manga/";
var actual = _defaultParser.Parse(@"E:/Manga/Accel World/cover.png", rootPath);
Assert.Null(actual);
}
[Fact]
public void Parse_ParseInfo_Manga()
@ -134,11 +126,12 @@ public class DefaultParserTests
filepath = @"E:\Manga\Beelzebub\Beelzebub_01_[Noodles].zip";
expected.Add(filepath, new ParserInfo
{
Series = "Beelzebub", Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume,
Series = "Beelzebub", Volumes = Parser.LooseLeafVolume,
Chapters = "1", Filename = "Beelzebub_01_[Noodles].zip", Format = MangaFormat.Archive,
FullFilePath = filepath
});
// Note: Lots of duplicates here. I think I can move them to the ParserTests itself
filepath = @"E:\Manga\Ichinensei ni Nacchattara\Ichinensei_ni_Nacchattara_v01_ch01_[Taruby]_v1.1.zip";
expected.Add(filepath, new ParserInfo
{
@ -198,7 +191,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
});
@ -258,7 +251,7 @@ public class DefaultParserTests
foreach (var file in expected.Keys)
{
var expectedInfo = expected[file];
var actual = _defaultParser.Parse(file, rootPath);
var actual = _defaultParser.Parse(file, rootPath, rootPath, LibraryType.Manga, null);
if (expectedInfo == null)
{
Assert.Null(actual);
@ -283,7 +276,7 @@ public class DefaultParserTests
}
}
[Fact]
//[Fact]
public void Parse_ParseInfo_Manga_ImageOnly()
{
// Images don't have root path as E:\Manga, but rather as the path of the folder
@ -296,7 +289,7 @@ public class DefaultParserTests
Chapters = "8", Filename = "13.jpg", Format = MangaFormat.Image,
FullFilePath = filepath, IsSpecial = false
};
var actual2 = _defaultParser.Parse(filepath, @"E:\Manga\Monster #8");
var actual2 = _defaultParser.Parse(filepath, @"E:\Manga\Monster #8", "E:/Manga", LibraryType.Manga, null);
Assert.NotNull(actual2);
_testOutputHelper.WriteLine($"Validating {filepath}");
Assert.Equal(expectedInfo2.Format, actual2.Format);
@ -322,7 +315,7 @@ public class DefaultParserTests
FullFilePath = filepath, IsSpecial = false
};
actual2 = _defaultParser.Parse(filepath, @"E:\Manga\Extra layer for no reason\");
actual2 = _defaultParser.Parse(filepath, @"E:\Manga\Extra layer for no reason\", "E:/Manga",LibraryType.Manga, null);
Assert.NotNull(actual2);
_testOutputHelper.WriteLine($"Validating {filepath}");
Assert.Equal(expectedInfo2.Format, actual2.Format);
@ -348,7 +341,7 @@ public class DefaultParserTests
FullFilePath = filepath, IsSpecial = false
};
actual2 = _defaultParser.Parse(filepath, @"E:\Manga\Extra layer for no reason\");
actual2 = _defaultParser.Parse(filepath, @"E:\Manga\Extra layer for no reason\", "E:/Manga", LibraryType.Manga, null);
Assert.NotNull(actual2);
_testOutputHelper.WriteLine($"Validating {filepath}");
Assert.Equal(expectedInfo2.Format, actual2.Format);
@ -379,7 +372,7 @@ public class DefaultParserTests
filesystem.AddFile(@"E:/Manga/Foo 50/Specials/Foo 50 SP01.cbz", new MockFileData(""));
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
var parser = new DefaultParser(ds);
var parser = new BasicParser(ds, new ImageParser(ds));
var filepath = @"E:/Manga/Foo 50/Foo 50 v1.cbz";
// There is a bad parse for series like "Foo 50", so we have parsed chapter as 50
@ -390,7 +383,7 @@ public class DefaultParserTests
FullFilePath = filepath
};
var actual = parser.Parse(filepath, rootPath);
var actual = parser.Parse(filepath, rootPath, rootPath, LibraryType.Manga, null);
Assert.NotNull(actual);
_testOutputHelper.WriteLine($"Validating {filepath}");
@ -414,12 +407,12 @@ 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
};
actual = parser.Parse(filepath, rootPath);
actual = parser.Parse(filepath, rootPath, rootPath, LibraryType.Manga, null);
Assert.NotNull(actual);
_testOutputHelper.WriteLine($"Validating {filepath}");
Assert.Equal(expected.Format, actual.Format);
@ -444,12 +437,12 @@ public class DefaultParserTests
[Fact]
public void Parse_ParseInfo_Comic()
{
const string rootPath = @"E:/Comics/";
const string rootPath = "E:/Comics/";
var expected = new Dictionary<string, ParserInfo>();
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
});
@ -482,7 +475,7 @@ public class DefaultParserTests
foreach (var file in expected.Keys)
{
var expectedInfo = expected[file];
var actual = _defaultParser.Parse(file, rootPath, LibraryType.Comic);
var actual = _defaultParser.Parse(file, rootPath, rootPath, LibraryType.Comic, null);
if (expectedInfo == null)
{
Assert.Null(actual);

View File

@ -0,0 +1,97 @@
using System.IO.Abstractions.TestingHelpers;
using API.Entities.Enums;
using API.Services;
using API.Services.Tasks.Scanner.Parser;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
namespace API.Tests.Parsers;
public class ImageParserTests
{
private readonly ImageParser _parser;
private readonly ILogger<DirectoryService> _dsLogger = Substitute.For<ILogger<DirectoryService>>();
private const string RootDirectory = "C:/Comics/";
public ImageParserTests()
{
var fileSystem = new MockFileSystem();
fileSystem.AddDirectory("C:/Comics/");
fileSystem.AddDirectory("C:/Comics/Birds of Prey (2002)");
fileSystem.AddFile("C:/Comics/Birds of Prey/Chapter 01/01.jpg", new MockFileData(""));
fileSystem.AddFile("C:/Comics/DC Comics/Birds of Prey/Chapter 01/01.jpg", new MockFileData(""));
var ds = new DirectoryService(_dsLogger, fileSystem);
_parser = new ImageParser(ds);
}
#region Parse
/// <summary>
/// Tests that if there is a Series Folder then Chapter folder, the code appropriately identifies the Series name and Chapter
/// </summary>
[Fact]
public void Parse_SeriesWithDirectoryName()
{
var actual = _parser.Parse("C:/Comics/Birds of Prey/Chapter 01/01.jpg", "C:/Comics/Birds of Prey/",
RootDirectory, LibraryType.Image, null);
Assert.NotNull(actual);
Assert.Equal("Birds of Prey", actual.Series);
Assert.Equal("1", actual.Chapters);
}
/// <summary>
/// Tests that if there is a Series Folder only, the code appropriately identifies the Series name from folder
/// </summary>
[Fact]
public void Parse_SeriesWithNoNestedChapter()
{
var actual = _parser.Parse("C:/Comics/Birds of Prey/Chapter 01 page 01.jpg", "C:/Comics/",
RootDirectory, LibraryType.Image, null);
Assert.NotNull(actual);
Assert.Equal("Birds of Prey", actual.Series);
Assert.Equal(Parser.DefaultChapter, actual.Chapters);
}
/// <summary>
/// Tests that if there is a Series Folder only, the code appropriately identifies the Series name from folder and everything else as a
/// </summary>
[Fact]
public void Parse_SeriesWithLooseImages()
{
var actual = _parser.Parse("C:/Comics/Birds of Prey/page 01.jpg", "C:/Comics/",
RootDirectory, LibraryType.Image, null);
Assert.NotNull(actual);
Assert.Equal("Birds of Prey", actual.Series);
Assert.Equal(Parser.DefaultChapter, actual.Chapters);
Assert.True(actual.IsSpecial);
}
#endregion
#region IsApplicable
/// <summary>
/// Tests that this Parser can only be used on images and Image library type
/// </summary>
[Fact]
public void IsApplicable_Fails_WhenNonMatchingLibraryType()
{
Assert.False(_parser.IsApplicable("something.cbz", LibraryType.Manga));
Assert.False(_parser.IsApplicable("something.cbz", LibraryType.Image));
Assert.False(_parser.IsApplicable("something.epub", LibraryType.Image));
}
/// <summary>
/// Tests that this Parser can only be used on images and Image library type
/// </summary>
[Fact]
public void IsApplicable_Success_WhenMatchingLibraryType()
{
Assert.True(_parser.IsApplicable("something.png", LibraryType.Image));
}
#endregion
}

View File

@ -0,0 +1,71 @@
using System.IO.Abstractions.TestingHelpers;
using API.Entities.Enums;
using API.Services;
using API.Services.Tasks.Scanner.Parser;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
namespace API.Tests.Parsers;
public class PdfParserTests
{
private readonly PdfParser _parser;
private readonly ILogger<DirectoryService> _dsLogger = Substitute.For<ILogger<DirectoryService>>();
private const string RootDirectory = "C:/Books/";
public PdfParserTests()
{
var fileSystem = new MockFileSystem();
fileSystem.AddDirectory("C:/Books/");
fileSystem.AddDirectory("C:/Books/Birds of Prey (2002)");
fileSystem.AddFile("C:/Books/A Dictionary of Japanese Food - Ingredients and Culture/A Dictionary of Japanese Food - Ingredients and Culture.pdf", new MockFileData(""));
fileSystem.AddFile("C:/Comics/DC Comics/Birds of Prey/Chapter 01/01.jpg", new MockFileData(""));
var ds = new DirectoryService(_dsLogger, fileSystem);
_parser = new PdfParser(ds);
}
#region Parse
/// <summary>
/// Tests that if there is a Series Folder then Chapter folder, the code appropriately identifies the Series name and Chapter
/// </summary>
[Fact]
public void Parse_Book_SeriesWithDirectoryName()
{
var actual = _parser.Parse("C:/Books/A Dictionary of Japanese Food - Ingredients and Culture/A Dictionary of Japanese Food - Ingredients and Culture.pdf",
"C:/Books/A Dictionary of Japanese Food - Ingredients and Culture/",
RootDirectory, LibraryType.Book, null);
Assert.NotNull(actual);
Assert.Equal("A Dictionary of Japanese Food - Ingredients and Culture", actual.Series);
Assert.Equal(Parser.DefaultChapter, actual.Chapters);
Assert.True(actual.IsSpecial);
}
#endregion
#region IsApplicable
/// <summary>
/// Tests that this Parser can only be used on pdfs
/// </summary>
[Fact]
public void IsApplicable_Fails_WhenNonMatchingLibraryType()
{
Assert.False(_parser.IsApplicable("something.cbz", LibraryType.Manga));
Assert.False(_parser.IsApplicable("something.cbz", LibraryType.Image));
Assert.False(_parser.IsApplicable("something.epub", LibraryType.Image));
Assert.False(_parser.IsApplicable("something.png", LibraryType.Book));
}
/// <summary>
/// Tests that this Parser can only be used on pdfs
/// </summary>
[Fact]
public void IsApplicable_Success_WhenMatchingLibraryType()
{
Assert.True(_parser.IsApplicable("something.pdf", LibraryType.Book));
Assert.True(_parser.IsApplicable("something.pdf", LibraryType.Manga));
}
#endregion
}

View File

@ -1,8 +1,8 @@
using Xunit;
namespace API.Tests.Parser;
namespace API.Tests.Parsing;
public class BookParserTests
public class BookParsingTests
{
[Theory]
[InlineData("Gifting The Wonderful World With Blessings! - 3 Side Stories [yuNS][Unknown]", "Gifting The Wonderful World With Blessings!")]

View File

@ -6,20 +6,18 @@ using NSubstitute;
using Xunit;
using Xunit.Abstractions;
namespace API.Tests.Parser;
namespace API.Tests.Parsing;
public class ComicParserTests
public class ComicParsingTests
{
private readonly ITestOutputHelper _testOutputHelper;
private readonly DefaultParser _defaultParser;
private static readonly string DefaultVolume = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume;
private readonly IDefaultParser _basicParser;
public ComicParserTests(ITestOutputHelper testOutputHelper)
public ComicParsingTests(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
_defaultParser =
new DefaultParser(new DirectoryService(Substitute.For<ILogger<DirectoryService>>(),
new MockFileSystem()));
var directoryService = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem());
_basicParser = new BasicParser(directoryService, new ImageParser(directoryService));
}
[Theory]
@ -210,8 +208,9 @@ public class ComicParserTests
[InlineData("Batman Beyond Omnibus (1999)", true)]
[InlineData("Batman Beyond Omnibus", true)]
[InlineData("01 Annual Batman Beyond", true)]
[InlineData("Blood Syndicate Annual #001", true)]
public void IsComicSpecialTest(string input, bool expected)
{
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.IsComicSpecial(input));
Assert.Equal(expected, Parser.IsComicSpecial(input));
}
}

View File

@ -0,0 +1,107 @@
using System.IO.Abstractions.TestingHelpers;
using API.Entities.Enums;
using API.Services;
using API.Services.Tasks.Scanner.Parser;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
using Xunit.Abstractions;
namespace API.Tests.Parsing;
public class ImageParsingTests
{
private readonly ITestOutputHelper _testOutputHelper;
private readonly ImageParser _parser;
public ImageParsingTests(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
var directoryService = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem());
_parser = new ImageParser(directoryService);
}
//[Fact]
public void Parse_ParseInfo_Manga_ImageOnly()
{
// Images don't have root path as E:\Manga, but rather as the path of the folder
// Note: Fallback to folder will parse Monster #8 and get Monster
var filepath = @"E:\Manga\Monster #8\Ch. 001-016 [MangaPlus] [Digital] [amit34521]\Monster #8 Ch. 001 [MangaPlus] [Digital] [amit34521]\13.jpg";
var expectedInfo2 = new ParserInfo
{
Series = "Monster #8", Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, Edition = "",
Chapters = "8", Filename = "13.jpg", Format = MangaFormat.Image,
FullFilePath = filepath, IsSpecial = false
};
var actual2 = _parser.Parse(filepath, @"E:\Manga\Monster #8", "E:/Manga", LibraryType.Image, null);
Assert.NotNull(actual2);
_testOutputHelper.WriteLine($"Validating {filepath}");
Assert.Equal(expectedInfo2.Format, actual2.Format);
_testOutputHelper.WriteLine("Format ✓");
Assert.Equal(expectedInfo2.Series, actual2.Series);
_testOutputHelper.WriteLine("Series ✓");
Assert.Equal(expectedInfo2.Chapters, actual2.Chapters);
_testOutputHelper.WriteLine("Chapters ✓");
Assert.Equal(expectedInfo2.Volumes, actual2.Volumes);
_testOutputHelper.WriteLine("Volumes ✓");
Assert.Equal(expectedInfo2.Edition, actual2.Edition);
_testOutputHelper.WriteLine("Edition ✓");
Assert.Equal(expectedInfo2.Filename, actual2.Filename);
_testOutputHelper.WriteLine("Filename ✓");
Assert.Equal(expectedInfo2.FullFilePath, actual2.FullFilePath);
_testOutputHelper.WriteLine("FullFilePath ✓");
filepath = @"E:\Manga\Extra layer for no reason\Just Images the second\Vol19\ch. 186\Vol. 19 p106.gif";
expectedInfo2 = new ParserInfo
{
Series = "Just Images the second", Volumes = "19", Edition = "",
Chapters = "186", Filename = "Vol. 19 p106.gif", Format = MangaFormat.Image,
FullFilePath = filepath, IsSpecial = false
};
actual2 = _parser.Parse(filepath, @"E:\Manga\Extra layer for no reason\", "E:/Manga", LibraryType.Image, null);
Assert.NotNull(actual2);
_testOutputHelper.WriteLine($"Validating {filepath}");
Assert.Equal(expectedInfo2.Format, actual2.Format);
_testOutputHelper.WriteLine("Format ✓");
Assert.Equal(expectedInfo2.Series, actual2.Series);
_testOutputHelper.WriteLine("Series ✓");
Assert.Equal(expectedInfo2.Chapters, actual2.Chapters);
_testOutputHelper.WriteLine("Chapters ✓");
Assert.Equal(expectedInfo2.Volumes, actual2.Volumes);
_testOutputHelper.WriteLine("Volumes ✓");
Assert.Equal(expectedInfo2.Edition, actual2.Edition);
_testOutputHelper.WriteLine("Edition ✓");
Assert.Equal(expectedInfo2.Filename, actual2.Filename);
_testOutputHelper.WriteLine("Filename ✓");
Assert.Equal(expectedInfo2.FullFilePath, actual2.FullFilePath);
_testOutputHelper.WriteLine("FullFilePath ✓");
filepath = @"E:\Manga\Extra layer for no reason\Just Images the second\Blank Folder\Vol19\ch. 186\Vol. 19 p106.gif";
expectedInfo2 = new ParserInfo
{
Series = "Just Images the second", Volumes = "19", Edition = "",
Chapters = "186", Filename = "Vol. 19 p106.gif", Format = MangaFormat.Image,
FullFilePath = filepath, IsSpecial = false
};
actual2 = _parser.Parse(filepath, @"E:\Manga\Extra layer for no reason\", "E:/Manga", LibraryType.Image, null);
Assert.NotNull(actual2);
_testOutputHelper.WriteLine($"Validating {filepath}");
Assert.Equal(expectedInfo2.Format, actual2.Format);
_testOutputHelper.WriteLine("Format ✓");
Assert.Equal(expectedInfo2.Series, actual2.Series);
_testOutputHelper.WriteLine("Series ✓");
Assert.Equal(expectedInfo2.Chapters, actual2.Chapters);
_testOutputHelper.WriteLine("Chapters ✓");
Assert.Equal(expectedInfo2.Volumes, actual2.Volumes);
_testOutputHelper.WriteLine("Volumes ✓");
Assert.Equal(expectedInfo2.Edition, actual2.Edition);
_testOutputHelper.WriteLine("Edition ✓");
Assert.Equal(expectedInfo2.Filename, actual2.Filename);
_testOutputHelper.WriteLine("Filename ✓");
Assert.Equal(expectedInfo2.FullFilePath, actual2.FullFilePath);
_testOutputHelper.WriteLine("FullFilePath ✓");
}
}

View File

@ -2,13 +2,13 @@ using API.Entities.Enums;
using Xunit;
using Xunit.Abstractions;
namespace API.Tests.Parser;
namespace API.Tests.Parsing;
public class MangaParserTests
public class MangaParsingTests
{
private readonly ITestOutputHelper _testOutputHelper;
public MangaParserTests(ITestOutputHelper testOutputHelper)
public MangaParsingTests(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
}
@ -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));

View File

@ -2,7 +2,7 @@
using API.Services.Tasks.Scanner.Parser;
using Xunit;
namespace API.Tests.Parser;
namespace API.Tests.Parsing;
public class ParserInfoTests
{

View File

@ -3,9 +3,9 @@ using System.Linq;
using Xunit;
using static API.Services.Tasks.Scanner.Parser.Parser;
namespace API.Tests.Parser;
namespace API.Tests.Parsing;
public class ParserTests
public class ParsingTests
{
[Fact]
public void ShouldWork()
@ -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));

View File

@ -52,12 +52,12 @@ internal class MockReadingItemServiceForCacheService : IReadingItemService
throw new System.NotImplementedException();
}
public ParserInfo Parse(string path, string rootPath, LibraryType type)
public ParserInfo Parse(string path, string rootPath, string libraryRoot, LibraryType type)
{
throw new System.NotImplementedException();
}
public ParserInfo ParseFile(string path, string rootPath, LibraryType type)
public ParserInfo ParseFile(string path, string rootPath, string libraryRoot, LibraryType type)
{
throw new System.NotImplementedException();
}
@ -156,7 +156,9 @@ public class CacheServiceTests
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
var cleanupService = new CacheService(_logger, _unitOfWork, ds,
new ReadingItemService(Substitute.For<IArchiveService>(),
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds), Substitute.For<IBookmarkService>());
Substitute.For<IBookService>(),
Substitute.For<IImageService>(), ds, Substitute.For<ILogger<ReadingItemService>>()),
Substitute.For<IBookmarkService>());
await ResetDB();
var s = new SeriesBuilder("Test").Build();
@ -231,7 +233,8 @@ public class CacheServiceTests
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
var cleanupService = new CacheService(_logger, _unitOfWork, ds,
new ReadingItemService(Substitute.For<IArchiveService>(),
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds), Substitute.For<IBookmarkService>());
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds, Substitute.For<ILogger<ReadingItemService>>()),
Substitute.For<IBookmarkService>());
cleanupService.CleanupChapters(new []{1, 3});
Assert.Empty(ds.GetFiles(CacheDirectory, searchOption:SearchOption.AllDirectories));
@ -252,7 +255,8 @@ public class CacheServiceTests
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
var cs = new CacheService(_logger, _unitOfWork, ds,
new ReadingItemService(Substitute.For<IArchiveService>(),
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds), Substitute.For<IBookmarkService>());
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds, Substitute.For<ILogger<ReadingItemService>>()),
Substitute.For<IBookmarkService>());
var c = new ChapterBuilder("1")
.WithFile(new MangaFileBuilder($"{DataDirectory}1.epub", MangaFormat.Epub).Build())
@ -292,7 +296,8 @@ public class CacheServiceTests
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
var cs = new CacheService(_logger, _unitOfWork, ds,
new ReadingItemService(Substitute.For<IArchiveService>(),
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds), Substitute.For<IBookmarkService>());
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds, Substitute.For<ILogger<ReadingItemService>>()),
Substitute.For<IBookmarkService>());
// Flatten to prepare for how GetFullPath expects
ds.Flatten($"{CacheDirectory}1/");
@ -335,7 +340,8 @@ public class CacheServiceTests
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
var cs = new CacheService(_logger, _unitOfWork, ds,
new ReadingItemService(Substitute.For<IArchiveService>(),
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds), Substitute.For<IBookmarkService>());
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds, Substitute.For<ILogger<ReadingItemService>>()),
Substitute.For<IBookmarkService>());
// Flatten to prepare for how GetFullPath expects
ds.Flatten($"{CacheDirectory}1/");
@ -375,7 +381,8 @@ public class CacheServiceTests
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
var cs = new CacheService(_logger, _unitOfWork, ds,
new ReadingItemService(Substitute.For<IArchiveService>(),
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds), Substitute.For<IBookmarkService>());
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds, Substitute.For<ILogger<ReadingItemService>>()),
Substitute.For<IBookmarkService>());
// Flatten to prepare for how GetFullPath expects
ds.Flatten($"{CacheDirectory}1/");
@ -419,7 +426,8 @@ public class CacheServiceTests
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
var cs = new CacheService(_logger, _unitOfWork, ds,
new ReadingItemService(Substitute.For<IArchiveService>(),
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds), Substitute.For<IBookmarkService>());
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds, Substitute.For<ILogger<ReadingItemService>>()),
Substitute.For<IBookmarkService>());
// Flatten to prepare for how GetFullPath expects
ds.Flatten($"{CacheDirectory}1/");

View File

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

View File

@ -721,6 +721,45 @@ public class DirectoryServiceTests
#endregion
#region FindLowestDirectoriesFromFiles
[Theory]
[InlineData(new [] {"C:/Manga/"},
new [] {"C:/Manga/Love Hina/Vol. 01.cbz"},
"C:/Manga/Love Hina")]
[InlineData(new [] {"C:/Manga/"},
new [] {"C:/Manga/Romance/Love Hina/Vol. 01.cbz"},
"C:/Manga/Romance/Love Hina")]
[InlineData(new [] {"C:/Manga/Dir 1/", "c://Manga/Dir 2/"},
new [] {"C:/Manga/Dir 1/Love Hina/Vol. 01.cbz"},
"C:/Manga/Dir 1/Love Hina")]
[InlineData(new [] {"C:/Manga/Dir 1/", "c://Manga/"},
new [] {"D:/Manga/Love Hina/Vol. 01.cbz", "D:/Manga/Vol. 01.cbz"},
null)]
[InlineData(new [] {"C:/Manga/"},
new [] {"C:/Manga//Love Hina/Vol. 01.cbz"},
"C:/Manga/Love Hina")]
[InlineData(new [] {@"C:\mount\drive\Library\Test Library\Comics\"},
new [] {@"C:\mount\drive\Library\Test Library\Comics\Bruce Lee (1994)\Bruce Lee #001 (1994).cbz"},
@"C:/mount/drive/Library/Test Library/Comics/Bruce Lee (1994)")]
public void FindLowestDirectoriesFromFilesTest(string[] rootDirectories, string[] files, string expectedDirectory)
{
var fileSystem = new MockFileSystem();
foreach (var directory in rootDirectories)
{
fileSystem.AddDirectory(directory);
}
foreach (var f in files)
{
fileSystem.AddFile(f, new MockFileData(""));
}
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
var actual = ds.FindLowestDirectoriesFromFiles(rootDirectories, files);
Assert.Equal(expectedDirectory, actual);
}
#endregion
#region GetFoldersTillRoot
[Theory]

View File

@ -54,99 +54,34 @@ internal class MockReadingItemService : IReadingItemService
throw new NotImplementedException();
}
public ParserInfo Parse(string path, string rootPath, LibraryType type)
public ParserInfo Parse(string path, string rootPath, string libraryRoot, LibraryType type)
{
return _defaultParser.Parse(path, rootPath, type);
return _defaultParser.Parse(path, rootPath, libraryRoot, type);
}
public ParserInfo ParseFile(string path, string rootPath, LibraryType type)
public ParserInfo ParseFile(string path, string rootPath, string libraryRoot, LibraryType type)
{
return _defaultParser.Parse(path, rootPath, type);
return _defaultParser.Parse(path, rootPath, libraryRoot, type);
}
}
public class ParseScannedFilesTests
public class ParseScannedFilesTests : AbstractDbTest
{
private readonly ILogger<ParseScannedFiles> _logger = Substitute.For<ILogger<ParseScannedFiles>>();
private readonly IUnitOfWork _unitOfWork;
private readonly DbConnection _connection;
private readonly DataContext _context;
private const string CacheDirectory = "C:/kavita/config/cache/";
private const string CoverImageDirectory = "C:/kavita/config/covers/";
private const string BackupDirectory = "C:/kavita/config/backups/";
private const string DataDirectory = "C:/data/";
public ParseScannedFilesTests()
{
var contextOptions = new DbContextOptionsBuilder()
.UseSqlite(CreateInMemoryDatabase())
.Options;
_connection = RelationalOptionsExtension.Extract(contextOptions).Connection;
_context = new DataContext(contextOptions);
Task.Run(SeedDb).GetAwaiter().GetResult();
_unitOfWork = new UnitOfWork(_context, Substitute.For<IMapper>(), null);
// Since ProcessFile relies on _readingItemService, we can implement our own versions of _readingItemService so we have control over how the calls work
}
#region Setup
private static DbConnection CreateInMemoryDatabase()
{
var connection = new SqliteConnection("Filename=:memory:");
connection.Open();
return connection;
}
private async Task<bool> SeedDb()
{
await _context.Database.MigrateAsync();
var filesystem = CreateFileSystem();
await Seed.SeedSettings(_context, new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem));
var setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.CacheDirectory).SingleAsync();
setting.Value = CacheDirectory;
setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BackupDirectory).SingleAsync();
setting.Value = BackupDirectory;
_context.ServerSetting.Update(setting);
_context.Library.Add(new LibraryBuilder("Manga")
.WithFolderPath(new FolderPathBuilder(DataDirectory).Build())
.Build());
return await _context.SaveChangesAsync() > 0;
}
private async Task ResetDB()
protected override async Task ResetDb()
{
_context.Series.RemoveRange(_context.Series.ToList());
await _context.SaveChangesAsync();
}
private static MockFileSystem CreateFileSystem()
{
var fileSystem = new MockFileSystem();
fileSystem.Directory.SetCurrentDirectory("C:/kavita/");
fileSystem.AddDirectory("C:/kavita/config/");
fileSystem.AddDirectory(CacheDirectory);
fileSystem.AddDirectory(CoverImageDirectory);
fileSystem.AddDirectory(BackupDirectory);
fileSystem.AddDirectory(DataDirectory);
return fileSystem;
}
#endregion
#region MergeName
// NOTE: I don't think I can test MergeName as it relies on Tracking Files, which is more complicated than I need
@ -219,6 +154,15 @@ public class ParseScannedFilesTests
#region ScanLibrariesForSeries
/// <summary>
/// Test that when a folder has 2 series with a localizedSeries, they combine into one final series
/// </summary>
// [Fact]
// public async Task ScanLibrariesForSeries_ShouldCombineSeries()
// {
// // TODO: Implement these unit tests
// }
[Fact]
public async Task ScanLibrariesForSeries_ShouldFindFiles()
{
@ -231,36 +175,42 @@ public class ParseScannedFilesTests
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
var psf = new ParseScannedFiles(Substitute.For<ILogger<ParseScannedFiles>>(), ds,
new MockReadingItemService(new DefaultParser(ds)), Substitute.For<IEventHub>());
new MockReadingItemService(new BasicParser(ds, new ImageParser(ds))), Substitute.For<IEventHub>());
var parsedSeries = new Dictionary<ParsedSeries, IList<ParserInfo>>();
Task TrackFiles(Tuple<bool, IList<ParserInfo>> parsedInfo)
{
var skippedScan = parsedInfo.Item1;
var parsedFiles = parsedInfo.Item2;
if (parsedFiles.Count == 0) return Task.CompletedTask;
var foundParsedSeries = new ParsedSeries()
{
Name = parsedFiles.First().Series,
NormalizedName = parsedFiles.First().Series.ToNormalized(),
Format = parsedFiles.First().Format
};
parsedSeries.Add(foundParsedSeries, parsedFiles);
return Task.CompletedTask;
}
// var parsedSeries = new Dictionary<ParsedSeries, IList<ParserInfo>>();
//
// Task TrackFiles(Tuple<bool, IList<ParserInfo>> parsedInfo)
// {
// var skippedScan = parsedInfo.Item1;
// var parsedFiles = parsedInfo.Item2;
// if (parsedFiles.Count == 0) return Task.CompletedTask;
//
// var foundParsedSeries = new ParsedSeries()
// {
// Name = parsedFiles.First().Series,
// NormalizedName = parsedFiles.First().Series.ToNormalized(),
// Format = parsedFiles.First().Format
// };
//
// parsedSeries.Add(foundParsedSeries, parsedFiles);
// return Task.CompletedTask;
// }
var library =
await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1,
LibraryIncludes.Folders | LibraryIncludes.FileTypes);
Assert.NotNull(library);
library.Type = LibraryType.Manga;
await psf.ScanLibrariesForSeries(library, new List<string>() {"C:/Data/"}, false, await _unitOfWork.SeriesRepository.GetFolderPathMap(1), TrackFiles);
var parsedSeries = await psf.ScanLibrariesForSeries(library, new List<string>() {"C:/Data/"}, false,
await _unitOfWork.SeriesRepository.GetFolderPathMap(1));
Assert.Equal(3, parsedSeries.Values.Count);
Assert.NotEmpty(parsedSeries.Keys.Where(p => p.Format == MangaFormat.Archive && p.Name.Equals("Accel World")));
// Assert.Equal(3, parsedSeries.Values.Count);
// Assert.NotEmpty(parsedSeries.Keys.Where(p => p.Format == MangaFormat.Archive && p.Name.Equals("Accel World")));
Assert.Equal(3, parsedSeries.Count);
Assert.NotEmpty(parsedSeries.Select(p => p.ParsedSeries).Where(p => p.Format == MangaFormat.Archive && p.Name.Equals("Accel World")));
}
#endregion
@ -289,18 +239,16 @@ public class ParseScannedFilesTests
var fileSystem = CreateTestFilesystem();
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
var psf = new ParseScannedFiles(Substitute.For<ILogger<ParseScannedFiles>>(), ds,
new MockReadingItemService(new DefaultParser(ds)), Substitute.For<IEventHub>());
new MockReadingItemService(new BasicParser(ds, new ImageParser(ds))), Substitute.For<IEventHub>());
var directoriesSeen = new HashSet<string>();
var library =
await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1,
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1,
LibraryIncludes.Folders | LibraryIncludes.FileTypes);
await psf.ProcessFiles("C:/Data/", true, await _unitOfWork.SeriesRepository.GetFolderPathMap(1),
(files, directoryPath) =>
var scanResults = psf.ProcessFiles("C:/Data/", true, await _unitOfWork.SeriesRepository.GetFolderPathMap(1), library);
foreach (var scanResult in scanResults)
{
directoriesSeen.Add(directoryPath);
return Task.CompletedTask;
}, library);
directoriesSeen.Add(scanResult.Folder);
}
Assert.Equal(2, directoriesSeen.Count);
}
@ -311,16 +259,20 @@ public class ParseScannedFilesTests
var fileSystem = CreateTestFilesystem();
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
var psf = new ParseScannedFiles(Substitute.For<ILogger<ParseScannedFiles>>(), ds,
new MockReadingItemService(new DefaultParser(ds)), Substitute.For<IEventHub>());
new MockReadingItemService(new BasicParser(ds, new ImageParser(ds))), Substitute.For<IEventHub>());
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1,
LibraryIncludes.Folders | LibraryIncludes.FileTypes);
Assert.NotNull(library);
var directoriesSeen = new HashSet<string>();
await psf.ProcessFiles("C:/Data/", false, await _unitOfWork.SeriesRepository.GetFolderPathMap(1),
(files, directoryPath) =>
var scanResults = psf.ProcessFiles("C:/Data/", false,
await _unitOfWork.SeriesRepository.GetFolderPathMap(1), library);
foreach (var scanResult in scanResults)
{
directoriesSeen.Add(directoryPath);
return Task.CompletedTask;
}, await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1,
LibraryIncludes.Folders | LibraryIncludes.FileTypes));
directoriesSeen.Add(scanResult.Folder);
}
Assert.Single(directoriesSeen);
directoriesSeen.TryGetValue("C:/Data/", out var actual);
@ -342,18 +294,14 @@ public class ParseScannedFilesTests
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
var psf = new ParseScannedFiles(Substitute.For<ILogger<ParseScannedFiles>>(), ds,
new MockReadingItemService(new DefaultParser(ds)), Substitute.For<IEventHub>());
new MockReadingItemService(new BasicParser(ds, new ImageParser(ds))), Substitute.For<IEventHub>());
var callCount = 0;
await psf.ProcessFiles("C:/Data", true, await _unitOfWork.SeriesRepository.GetFolderPathMap(1),(files, folderPath) =>
{
callCount++;
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1,
LibraryIncludes.Folders | LibraryIncludes.FileTypes);
Assert.NotNull(library);
var scanResults = psf.ProcessFiles("C:/Data", true, await _unitOfWork.SeriesRepository.GetFolderPathMap(1), library);
return Task.CompletedTask;
}, await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1,
LibraryIncludes.Folders | LibraryIncludes.FileTypes));
Assert.Equal(2, callCount);
Assert.Equal(2, scanResults.Count);
}
@ -375,18 +323,19 @@ public class ParseScannedFilesTests
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
var psf = new ParseScannedFiles(Substitute.For<ILogger<ParseScannedFiles>>(), ds,
new MockReadingItemService(new DefaultParser(ds)), Substitute.For<IEventHub>());
new MockReadingItemService(new BasicParser(ds, new ImageParser(ds))), Substitute.For<IEventHub>());
var callCount = 0;
await psf.ProcessFiles("C:/Data", false, await _unitOfWork.SeriesRepository.GetFolderPathMap(1),(files, folderPath) =>
{
callCount++;
return Task.CompletedTask;
}, await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1,
LibraryIncludes.Folders | LibraryIncludes.FileTypes));
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1,
LibraryIncludes.Folders | LibraryIncludes.FileTypes);
Assert.NotNull(library);
var scanResults = psf.ProcessFiles("C:/Data", false,
await _unitOfWork.SeriesRepository.GetFolderPathMap(1), library);
Assert.Equal(1, callCount);
Assert.Single(scanResults);
}
#endregion
}

View File

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

View File

@ -1205,6 +1205,65 @@ public class ReadingListServiceTests
Assert.Equal(2, createdList.Items.First(item => item.Order == 2).ChapterId);
Assert.Equal(4, createdList.Items.First(item => item.Order == 3).ChapterId);
}
/// <summary>
/// This test is about ensuring Annuals that are a separate series can be linked up properly (ComicVine)
/// </summary>
//[Fact]
public async Task CreateReadingListFromCBL_ShouldCreateList_WithAnnuals()
{
// TODO: Implement this correctly
await ResetDb();
var cblReadingList = LoadCblFromPath("Annual.cbl");
// Mock up our series
var fablesSeries = new SeriesBuilder("Fables")
.WithVolume(new VolumeBuilder("2002")
.WithMinNumber(1)
.WithChapter(new ChapterBuilder("1").Build())
.WithChapter(new ChapterBuilder("2").Build())
.WithChapter(new ChapterBuilder("3").Build())
.Build())
.Build();
var fables2Series = new SeriesBuilder("Fables Annual")
.WithVolume(new VolumeBuilder("2003")
.WithMinNumber(1)
.WithChapter(new ChapterBuilder("1").Build())
.Build())
.Build();
_context.AppUser.Add(new AppUser()
{
UserName = "majora2007",
ReadingLists = new List<ReadingList>(),
Libraries = new List<Library>()
{
new LibraryBuilder("Test LIb 2", LibraryType.Book)
.WithSeries(fablesSeries)
.WithSeries(fables2Series)
.Build()
},
});
await _unitOfWork.CommitAsync();
var importSummary = await _readingListService.CreateReadingListFromCbl(1, cblReadingList);
Assert.Equal(CblImportResult.Success, importSummary.Success);
Assert.NotEmpty(importSummary.Results);
var createdList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(1);
Assert.NotNull(createdList);
Assert.Equal("Annual", createdList.Title);
Assert.Equal(4, createdList.Items.Count);
Assert.Equal(1, createdList.Items.First(item => item.Order == 0).ChapterId);
Assert.Equal(2, createdList.Items.First(item => item.Order == 1).ChapterId);
Assert.Equal(4, createdList.Items.First(item => item.Order == 2).ChapterId);
Assert.Equal(3, createdList.Items.First(item => item.Order == 3).ChapterId);
}
#endregion
#region CreateReadingListsFromSeries

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,19 @@
<?xml version="1.0"?>
<ReadingList xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Name>Fables</Name>
<Books>
<Book Series="Fables" Number="1" Volume="2002" Year="2002">
<Id>5bd3dd55-2a85-4325-aefa-21e9f19b12c9</Id>
</Book>
<Book Series="Fables" Number="2" Volume="2002" Year="2002">
<Id>3831761c-604a-4420-bed2-9f5ac4e94bd4</Id>
</Book>
<Book Series="Fables Annual" Number="1" Volume="2003" Year="2003" Format="Annual">
<Id>23acefd4-1bc7-4c3c-99df-133045d1f266</Id>
</Book>
<Book Series="Fables" Number="3" Volume="2002" Year="2002">
<Id>27a5d7db-9f7e-4be1-aca6-998a1cc1488f</Id>
</Book>
</Books>
<Matchers />
</ReadingList>

View File

@ -53,30 +53,30 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="30.1.0" />
<PackageReference Include="MailKit" Version="4.3.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1">
<PackageReference Include="CsvHelper" Version="31.0.2" />
<PackageReference Include="MailKit" Version="4.4.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="Docnet.Core" Version="2.6.0" />
<PackageReference Include="EasyCaching.InMemory" Version="1.9.2" />
<PackageReference Include="ExCSS" Version="4.2.4" />
<PackageReference Include="ExCSS" Version="4.2.5" />
<PackageReference Include="Flurl" Version="3.0.7" />
<PackageReference Include="Flurl.Http" Version="3.2.4" />
<PackageReference Include="Hangfire" Version="1.8.9" />
<PackageReference Include="Hangfire.InMemory" Version="0.7.0" />
<PackageReference Include="Hangfire" Version="1.8.11" />
<PackageReference Include="Hangfire.InMemory" Version="0.8.0" />
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.4.0" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.58" />
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.4.1" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.59" />
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.9" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.11" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" />
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
@ -94,16 +94,16 @@
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
<PackageReference Include="SharpCompress" Version="0.36.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.2" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.19.0.84025">
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.3" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.21.0.86780">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.3.1" />
<PackageReference Include="System.IO.Abstractions" Version="20.0.15" />
<PackageReference Include="System.Drawing.Common" Version="8.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.4.0" />
<PackageReference Include="System.IO.Abstractions" Version="20.0.28" />
<PackageReference Include="System.Drawing.Common" Version="8.0.3" />
<PackageReference Include="VersOne.Epub" Version="3.3.1" />
</ItemGroup>

View File

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

View File

@ -33,13 +33,14 @@ public class CblController : BaseApiController
/// <param name="file">FormBody with parameter name of cbl</param>
/// <returns></returns>
[HttpPost("validate")]
public async Task<ActionResult<CblImportSummaryDto>> ValidateCbl([FromForm(Name = "cbl")] IFormFile file)
public async Task<ActionResult<CblImportSummaryDto>> ValidateCbl([FromForm(Name = "cbl")] IFormFile file,
[FromForm(Name = "comicVineMatching")] bool comicVineMatching = false)
{
var userId = User.GetUserId();
try
{
var cbl = await SaveAndLoadCblFile(file);
var importSummary = await _readingListService.ValidateCblFile(userId, cbl);
var importSummary = await _readingListService.ValidateCblFile(userId, cbl, comicVineMatching);
importSummary.FileName = file.FileName;
return Ok(importSummary);
}
@ -83,13 +84,14 @@ public class CblController : BaseApiController
/// <param name="dryRun">If true, will only emulate the import but not perform. This should be done to preview what will happen</param>
/// <returns></returns>
[HttpPost("import")]
public async Task<ActionResult<CblImportSummaryDto>> ImportCbl([FromForm(Name = "cbl")] IFormFile file, [FromForm(Name = "dryRun")] bool dryRun = false)
public async Task<ActionResult<CblImportSummaryDto>> ImportCbl([FromForm(Name = "cbl")] IFormFile file,
[FromForm(Name = "dryRun")] bool dryRun = false, [FromForm(Name = "comicVineMatching")] bool comicVineMatching = false)
{
try
{
var userId = User.GetUserId();
var cbl = await SaveAndLoadCblFile(file);
var importSummary = await _readingListService.CreateReadingListFromCbl(userId, cbl, dryRun);
var importSummary = await _readingListService.CreateReadingListFromCbl(userId, cbl, dryRun, comicVineMatching);
importSummary.FileName = file.FileName;
return Ok(importSummary);
} catch (ArgumentNullException)

View File

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

View File

@ -32,7 +32,11 @@ public class LicenseController(
public async Task<ActionResult<bool>> HasValidLicense(bool forceCheck = false)
{
var result = await licenseService.HasActiveLicense(forceCheck);
await taskScheduler.ScheduleKavitaPlusTasks();
if (result)
{
await taskScheduler.ScheduleKavitaPlusTasks();
}
return Ok(result);
}

View File

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

View File

@ -13,14 +13,25 @@ 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>
/// <remarks>This can be something like 19.HU or Alpha as some comics are like this</remarks>
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!;
/// <summary>
/// This may be 0 under the circumstance that the Issue is "Alpha" or other non-standard numbers.
/// </summary>
public float MinNumber { get; init; }
public float MaxNumber { get; init; }
/// <summary>
/// The sorting order of the Chapter. Inherits from MinNumber, but can be overridden.
/// </summary>
public float SortOrder { get; set; }
/// <summary>
/// Total number of pages in all MangaFiles
/// </summary>
public int Pages { get; init; }

View File

@ -48,6 +48,9 @@ public enum FilterField
/// <summary>
/// Average rating from Kavita+ - Not usable for non-licensed users
/// </summary>
AverageRating = 28
AverageRating = 28,
Imprint = 29,
Team = 30,
Location = 31
}

View File

@ -18,10 +18,13 @@ public class ChapterMetadataDto
public ICollection<PersonDto> Characters { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Pencillers { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Inkers { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Imprints { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Colorists { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Letterers { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Editors { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Translators { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Teams { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Locations { get; set; } = new List<PersonDto>();
public ICollection<GenreTagDto> Genres { get; set; } = new List<GenreTagDto>();

View File

@ -1,4 +1,5 @@
using System.Xml.Serialization;
using API.Data.Metadata;
namespace API.DTOs.ReadingLists.CBL;
@ -21,6 +22,12 @@ public class CblBook
[XmlAttribute("Year")]
public string Year { get; set; }
/// <summary>
/// Main Series, Annual, Limited Series
/// </summary>
/// <remarks>This maps to <see cref="ComicInfo">Format</see> tag</remarks>
[XmlAttribute("Format")]
public string Format { get; set; }
/// <summary>
/// The underlying filetype
/// </summary>
/// <remarks>This is not part of the standard and explicitly for Kavita to support non cbz/cbr files</remarks>

View File

@ -22,4 +22,5 @@ public class RelatedSeriesDto
public IEnumerable<SeriesDto> Doujinshis { get; set; } = default!;
public IEnumerable<SeriesDto> Parent { get; set; } = default!;
public IEnumerable<SeriesDto> Editions { get; set; } = default!;
public IEnumerable<SeriesDto> Annuals { get; set; } = default!;
}

View File

@ -17,4 +17,5 @@ public class UpdateRelatedSeriesDto
public IList<int> AlternativeVersions { get; set; } = default!;
public IList<int> Doujinshis { get; set; } = default!;
public IList<int> Editions { get; set; } = default!;
public IList<int> Annuals { get; set; } = default!;
}

View File

@ -30,10 +30,14 @@ public class SeriesMetadataDto
public ICollection<PersonDto> Characters { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Pencillers { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Inkers { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Imprints { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Colorists { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Letterers { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Editors { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Translators { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Teams { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Locations { get; set; } = new List<PersonDto>();
/// <summary>
/// Highest Age Rating from all Chapters
/// </summary>
@ -80,10 +84,13 @@ public class SeriesMetadataDto
public bool ColoristLocked { get; set; }
public bool EditorLocked { get; set; }
public bool InkerLocked { get; set; }
public bool ImprintLocked { get; set; }
public bool LettererLocked { get; set; }
public bool PencillerLocked { get; set; }
public bool PublisherLocked { get; set; }
public bool TranslatorLocked { get; set; }
public bool TeamLocked { get; set; }
public bool LocationLocked { get; set; }
public bool CoverArtistLocked { get; set; }
public bool ReleaseYearLocked { get; set; }

View File

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

View File

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

View File

@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using 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);
}
}

View File

@ -156,10 +156,15 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
{
if (e.FromQuery || e.Entry.State != EntityState.Added || e.Entry.Entity is not IEntityDate entity) return;
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)

View File

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

View File

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

View File

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

View File

@ -0,0 +1,55 @@
using System;
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;
/// <summary>
/// v0.8.0 changed the range to that it doesn't have filename by default
/// </summary>
public static class MigrateChapterRange
{
public static async Task Migrate(DataContext dataContext, IUnitOfWork unitOfWork, ILogger<Program> logger)
{
if (await dataContext.ManualMigrationHistory.AnyAsync(m => m.Name == "MigrateChapterRange"))
{
return;
}
logger.LogCritical(
"Running MigrateChapterRange migration - Please be patient, this may take some time. This is not an error");
var chapters = await dataContext.Chapter.ToListAsync();
foreach (var chapter in chapters)
{
if (Parser.MinNumberFromRange(chapter.Range) == 0.0f)
{
chapter.Range = chapter.GetNumberTitle();
}
}
// Save changes after processing all series
if (dataContext.ChangeTracker.HasChanges())
{
await dataContext.SaveChangesAsync();
}
dataContext.ManualMigrationHistory.Add(new ManualMigrationHistory()
{
Name = "MigrateChapterRange",
ProductVersion = BuildInfo.Version.ToString(),
RanAt = DateTime.UtcNow
});
await dataContext.SaveChangesAsync();
logger.LogCritical(
"Running MigrateChapterRange migration - Completed. This is not an error");
}
}

View File

@ -15,9 +15,8 @@ public static class MigrateLibrariesToHaveAllFileTypes
{
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> logger)
{
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;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -13,8 +13,13 @@ namespace API.Data.ManualMigrations;
/// </summary>
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(

View File

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

View File

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

View File

@ -127,7 +127,11 @@ public class ComicInfo
public string CoverArtist { get; set; } = string.Empty;
public string Editor { get; set; } = string.Empty;
public string Publisher { get; set; } = string.Empty;
public string Imprint { get; set; } = string.Empty;
public string Characters { get; set; } = string.Empty;
public string Teams { get; set; } = string.Empty;
public string Locations { get; set; } = string.Empty;
public static AgeRating ConvertAgeRatingToEnum(string value)
{
@ -151,9 +155,12 @@ public class ComicInfo
info.Letterer = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Letterer);
info.Penciller = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Penciller);
info.Publisher = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Publisher);
info.Imprint = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Imprint);
info.Characters = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Characters);
info.Translator = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Translator);
info.CoverArtist = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.CoverArtist);
info.Teams = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Teams);
info.Locations = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Locations);
// We need to convert GTIN to ISBN
if (!string.IsNullOrEmpty(info.GTIN))
@ -174,7 +181,12 @@ public class ComicInfo
if (!string.IsNullOrEmpty(info.Number))
{
info.Number = info.Number.Replace(",", "."); // Corrective measure for non English OSes
info.Number = info.Number.Trim().Replace(",", "."); // Corrective measure for non English OSes
}
if (!string.IsNullOrEmpty(info.Volume))
{
info.Volume = info.Volume.Trim();
}
}

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class TeamsAndLocations : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "LocationLocked",
table: "SeriesMetadata",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "TeamLocked",
table: "SeriesMetadata",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "LocationLocked",
table: "SeriesMetadata");
migrationBuilder.DropColumn(
name: "TeamLocked",
table: "SeriesMetadata");
}
}
}

View File

@ -15,7 +15,7 @@ namespace API.Data.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.1");
modelBuilder.HasAnnotation("ProductVersion", "8.0.3");
modelBuilder.Entity("API.Entities.AppRole", b =>
{
@ -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");
@ -1241,6 +1253,9 @@ namespace API.Data.Migrations
b.Property<bool>("GenresLocked")
.HasColumnType("INTEGER");
b.Property<bool>("ImprintLocked")
.HasColumnType("INTEGER");
b.Property<bool>("InkerLocked")
.HasColumnType("INTEGER");
@ -1253,6 +1268,9 @@ namespace API.Data.Migrations
b.Property<bool>("LettererLocked")
.HasColumnType("INTEGER");
b.Property<bool>("LocationLocked")
.HasColumnType("INTEGER");
b.Property<int>("MaxCount")
.HasColumnType("INTEGER");
@ -1290,6 +1308,9 @@ namespace API.Data.Migrations
b.Property<bool>("TagsLocked")
.HasColumnType("INTEGER");
b.Property<bool>("TeamLocked")
.HasColumnType("INTEGER");
b.Property<int>("TotalCount")
.HasColumnType("INTEGER");
@ -1665,6 +1686,9 @@ namespace API.Data.Migrations
b.Property<bool>("LocalizedNameLocked")
.HasColumnType("INTEGER");
b.Property<string>("LowestFolderPath")
.HasColumnType("TEXT");
b.Property<int>("MaxHoursToRead")
.HasColumnType("INTEGER");
@ -1839,6 +1863,9 @@ namespace API.Data.Migrations
b.Property<DateTime>("LastModifiedUtc")
.HasColumnType("TEXT");
b.Property<string>("LookupName")
.HasColumnType("TEXT");
b.Property<int>("MaxHoursToRead")
.HasColumnType("INTEGER");

View File

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

View File

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

View File

@ -34,6 +34,7 @@ public interface IExternalSeriesMetadataRepository
Task<bool> ExternalSeriesMetadataNeedsRefresh(int seriesId);
Task<SeriesDetailPlusDto> GetSeriesDetailPlusDto(int seriesId);
Task LinkRecommendationsToSeries(Series series);
Task LinkRecommendationsToSeries(int seriesId);
Task<bool> IsBlacklistedSeries(int seriesId);
Task CreateBlacklistedSeries(int seriesId, bool saveChanges = true);
Task RemoveFromBlacklist(int seriesId);
@ -179,6 +180,13 @@ public class ExternalSeriesMetadataRepository : IExternalSeriesMetadataRepositor
return seriesDetailPlusDto;
}
public async Task LinkRecommendationsToSeries(int seriesId)
{
var series = await _context.Series.Where(s => s.Id == seriesId).AsNoTracking().SingleOrDefaultAsync();
if (series == null) return;
await LinkRecommendationsToSeries(series);
}
/// <summary>
/// Searches Recommendations without a SeriesId on record and attempts to link based on Series Name/Localized Name
/// </summary>

View File

@ -318,7 +318,7 @@ public class LibraryRepository : ILibraryRepository
/// <returns></returns>
public async Task<bool> DoAnySeriesFoldersMatch(IEnumerable<string> folders)
{
var normalized = folders.Select(Services.Tasks.Scanner.Parser.Parser.NormalizePath);
var normalized = folders.Select(Parser.NormalizePath);
return await _context.Series.AnyAsync(s => normalized.Contains(s.FolderPath));
}

View File

@ -498,6 +498,7 @@ public class SeriesRepository : ISeriesRepository
.Include(c => c.Files)
.Where(c => EF.Functions.Like(c.TitleName, $"%{searchQuery}%")
|| EF.Functions.Like(c.ISBN, $"%{searchQuery}%")
|| EF.Functions.Like(c.Range, $"%{searchQuery}%")
)
.Where(c => c.Files.All(f => fileIds.Contains(f.Id)))
.AsSplitQuery()
@ -1183,6 +1184,9 @@ public class SeriesRepository : ISeriesRepository
FilterField.Letterer => query.HasPeople(true, statement.Comparison, (IList<int>) value),
FilterField.Colorist => query.HasPeople(true, statement.Comparison, (IList<int>) value),
FilterField.Inker => query.HasPeople(true, statement.Comparison, (IList<int>) value),
FilterField.Imprint => query.HasPeople(true, statement.Comparison, (IList<int>) value),
FilterField.Team => query.HasPeople(true, statement.Comparison, (IList<int>) value),
FilterField.Location => query.HasPeople(true, statement.Comparison, (IList<int>) value),
FilterField.Penciller => query.HasPeople(true, statement.Comparison, (IList<int>) value),
FilterField.Writers => query.HasPeople(true, statement.Comparison, (IList<int>) value),
FilterField.Genres => query.HasGenre(true, statement.Comparison, (IList<int>) value),
@ -1817,19 +1821,7 @@ public class SeriesRepository : ISeriesRepository
AlternativeSettings = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.AlternativeSetting, userRating),
AlternativeVersions = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.AlternativeVersion, userRating),
Doujinshis = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Doujinshi, userRating),
// Parent = await _context.Series
// .SelectMany(s =>
// s.TargetSeries.Where(r => r.TargetSeriesId == seriesId
// && usersSeriesIds.Contains(r.TargetSeriesId)
// && r.RelationKind != RelationKind.Prequel
// && r.RelationKind != RelationKind.Sequel
// && r.RelationKind != RelationKind.Edition)
// .Select(sr => sr.Series))
// .RestrictAgainstAgeRestriction(userRating)
// .AsSplitQuery()
// .AsNoTracking()
// .ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
// .ToListAsync(),
Annuals = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Annual, userRating),
Parent = await _context.SeriesRelation
.Where(r => r.TargetSeriesId == seriesId
&& usersSeriesIds.Contains(r.TargetSeriesId)
@ -1891,8 +1883,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,
@ -2063,7 +2055,7 @@ public class SeriesRepository : ISeriesRepository
foreach (var series in info)
{
if (series.FolderPath == null) continue;
if (!map.ContainsKey(series.FolderPath))
if (!map.TryGetValue(series.FolderPath, out var value))
{
map.Add(series.FolderPath, new List<SeriesModified>()
{
@ -2072,9 +2064,8 @@ public class SeriesRepository : ISeriesRepository
}
else
{
map[series.FolderPath].Add(series);
value.Add(series);
}
}
return map;

View File

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

View File

@ -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,48 @@ public class Chapter : IEntityDate, IHasReadTimeEstimate
if (IsSpecial)
{
Number = Parser.DefaultChapter;
MinNumber = Parser.DefaultChapterNumber;
MaxNumber = Parser.DefaultChapterNumber;
}
// NOTE: This doesn't work well for all because Pdf usually should use into.Title or even filename
Title = (IsSpecial && info.Format == MangaFormat.Epub)
? info.Title
: Range;
: Parser.RemoveExtensionIfSupported(Range);
var specialTreatment = info.IsSpecialInfo();
Range = specialTreatment ? info.Filename : info.Chapters;
}
/// <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 Parser.RemoveExtensionIfSupported(Title);
}
if (MinNumber.Is(0) && !float.TryParse(Range, out _))
{
return $"{Range}";
}
return $"{MinNumber}";
}
return $"{MinNumber}-{MaxNumber}";
}
/// <summary>
/// Is the Chapter representing a single Volume (volume 1.cbz). If so, Min/Max will be Default and will not be special
/// </summary>
/// <returns></returns>
public bool IsSingleVolumeChapter()
{
return MinNumber.Is(Parser.DefaultChapterNumber) && !IsSpecial;
}
}

View File

@ -29,4 +29,10 @@ public enum LibraryType
/// </summary>
[Description("Light Novel")]
LightNovel = 4,
/// <summary>
/// Uses Comic regex for filename parsing, uses ComicVine type of Parsing. Will replace Comic type in future
/// </summary>
[Description("Comic (ComicVine)")]
ComicVine = 5,
}

View File

@ -24,7 +24,11 @@ public enum PersonRole
/// <summary>
/// The Translator
/// </summary>
Translator = 12
Translator = 12,
/// <summary>
/// The publisher before another Publisher bought
/// </summary>
Imprint = 13,
Team = 14,
Location = 15
}

View File

@ -71,6 +71,11 @@ public enum RelationKind
/// Same story, could be translation, colorization... Different edition of the series
/// </summary>
[Description("Edition")]
Edition = 13
Edition = 13,
/// <summary>
/// The target series is an annual of the Series
/// </summary>
[Description("Annual")]
Annual = 14
}

View File

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

View File

@ -68,14 +68,16 @@ public class SeriesMetadata : IHasConcurrencyToken
public bool ColoristLocked { get; set; }
public bool EditorLocked { get; set; }
public bool InkerLocked { get; set; }
public bool ImprintLocked { get; set; }
public bool LettererLocked { get; set; }
public bool PencillerLocked { get; set; }
public bool PublisherLocked { get; set; }
public bool TranslatorLocked { get; set; }
public bool TeamLocked { get; set; }
public bool LocationLocked { get; set; }
public bool CoverArtistLocked { get; set; }
public bool ReleaseYearLocked { get; set; }
// Relationship
public Series Series { get; set; } = null!;
public int SeriesId { get; set; }

View File

@ -64,6 +64,11 @@ public class Series : IEntityDate, IHasReadTimeEstimate
/// <remarks><see cref="Services.Tasks.Scanner.Parser.Parser.NormalizePath"/> must be used before setting</remarks>
public string? FolderPath { get; set; }
/// <summary>
/// Lowest path (that is under library root) that contains all files for the series.
/// </summary>
/// <remarks><see cref="Services.Tasks.Scanner.Parser.Parser.NormalizePath"/> must be used before setting</remarks>
public string? LowestFolderPath { get; set; }
/// <summary>
/// Last time the folder was scanned
/// </summary>
public DateTime LastFolderScanned { get; set; }

View File

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

View File

@ -60,6 +60,7 @@ public static class ApplicationServiceExtensions
services.AddScoped<ILibraryWatcher, LibraryWatcher>();
services.AddScoped<ITachiyomiService, TachiyomiService>();
services.AddScoped<ICollectionTagService, CollectionTagService>();
services.AddScoped<ITagManagerService, TagManagerService>();
services.AddScoped<IFileSystem, FileSystem>();
services.AddScoped<IDirectoryService, DirectoryService>();

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using API.Entities;
using API.Helpers;
@ -28,10 +29,11 @@ public static class ChapterListExtensions
/// <returns></returns>
public static Chapter? GetChapterByRange(this IEnumerable<Chapter> chapters, ParserInfo info)
{
var normalizedPath = Parser.NormalizePath(info.FullFilePath);
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 == info.Chapters);
return specialTreatment
? chapters.FirstOrDefault(c => c.Range == Parser.RemoveExtensionIfSupported(info.Filename) || c.Files.Select(f => Parser.NormalizePath(f.FilePath)).Contains(normalizedPath))
: chapters.FirstOrDefault(c => c.Range == info.Chapters);
}
/// <summary>
@ -41,6 +43,6 @@ public static class ChapterListExtensions
/// <returns></returns>
public static int MinimumReleaseYear(this IList<Chapter> chapters)
{
return chapters.Select(v => v.ReleaseDate.Year).Where(y => NumberHelper.IsValidYear(y)).DefaultIfEmpty().Min();
return chapters.Select(v => v.ReleaseDate.Year).Where(NumberHelper.IsValidYear).DefaultIfEmpty().Min();
}
}

View File

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

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using API.Entities;
using API.Services.Tasks.Scanner.Parser;
@ -27,7 +28,9 @@ public static class ParserInfoListExtensions
/// <returns></returns>
public static bool HasInfo(this IList<ParserInfo> infos, Chapter chapter)
{
return chapter.IsSpecial ? infos.Any(v => v.Filename == chapter.Range)
: infos.Any(v => v.Chapters == chapter.Range);
var chapterFiles = chapter.Files.Select(x => Parser.NormalizePath(x.FilePath)).ToList();
var infoFiles = infos.Select(x => Parser.NormalizePath(x.FullFilePath)).ToList();
return infoFiles.Intersect(chapterFiles).Any();
}
}

View File

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

View File

@ -1,6 +1,4 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Linq;
using API.Comparators;
using API.Entities;
using API.Services.Tasks.Scanner.Parser;
@ -19,13 +17,19 @@ 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;
// If first volume here is specials, move to the next as specials should almost always be last.
if (firstVolume.MinNumber.Is(Parser.SpecialVolumeNumber) && volumes.Count > 1)
{
firstVolume = volumes[1];
}
var chapters = firstVolume.Chapters
.OrderBy(c => c.Number.AsDouble(), ChapterSortComparerZeroFirst.Default)
.OrderBy(c => c.SortOrder)
.ToList();
if (chapters.Count > 1 && chapters.Exists(c => c.IsSpecial))
@ -34,32 +38,42 @@ public static class SeriesExtensions
}
// just volumes
if (volumes.TrueForAll(v => $"{v.MinNumber}" != Parser.LooseLeafVolume))
if (volumes.TrueForAll(v => v.MinNumber.IsNot(Parser.LooseLeafVolumeNumber)))
{
return firstVolume.CoverImage;
}
// If we have loose leaf chapters
// if loose leaf chapters AND volumes, just return first volume
if (volumes.Count >= 1 && $"{volumes[0].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.SortOrder)
.ToList();
if (looseLeafChapters.Count > 0 && (1.0f * volumes[0].MinNumber) > looseLeafChapters[0].Number.AsFloat())
if (looseLeafChapters.Count > 0 && volumes[0].MinNumber > looseLeafChapters[0].MinNumber)
{
var first = looseLeafChapters.Find(c => c.SortOrder.Is(1f));
if (first != null) return first.CoverImage;
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(c => !c.IsSpecial)
.OrderBy(c => c.MinNumber, ChapterSortComparerDefaultLast.Default)
.ToList();
return firstLooseLeafChapter?.CoverImage ?? firstVolume.CoverImage;
var exactlyChapter1 = chpts.Find(c => c.MinNumber.Is(1f));
if (exactlyChapter1 != null)
{
return exactlyChapter1.CoverImage;
}
return chpts.FirstOrDefault()?.CoverImage ?? firstVolume.CoverImage;
}
}

View File

@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic;
using System.Collections.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,26 @@ 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));
}
/// <summary>
/// Returns the first (and only) special volume or null if none
/// </summary>
/// <param name="volumes"></param>
/// <returns></returns>
public static Volume? GetSpecialVolumeOrDefault(this IEnumerable<Volume> volumes)
{
return volumes.FirstOrDefault(v => v.MinNumber.Is(Parser.SpecialVolumeNumber));
}
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));
}
}

View File

@ -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>();
@ -128,6 +128,14 @@ public class AutoMapperProfiles : Profile
opt =>
opt.MapFrom(
src => src.People.Where(p => p.Role == PersonRole.Editor).OrderBy(p => p.NormalizedName)))
.ForMember(dest => dest.Teams,
opt =>
opt.MapFrom(
src => src.People.Where(p => p.Role == PersonRole.Team).OrderBy(p => p.NormalizedName)))
.ForMember(dest => dest.Locations,
opt =>
opt.MapFrom(
src => src.People.Where(p => p.Role == PersonRole.Location).OrderBy(p => p.NormalizedName)))
.ForMember(dest => dest.Genres,
opt =>
opt.MapFrom(
@ -154,6 +162,9 @@ public class AutoMapperProfiles : Profile
.ForMember(dest => dest.Inkers,
opt =>
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Inker).OrderBy(p => p.NormalizedName)))
.ForMember(dest => dest.Imprints,
opt =>
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Imprint).OrderBy(p => p.NormalizedName)))
.ForMember(dest => dest.Letterers,
opt =>
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Letterer).OrderBy(p => p.NormalizedName)))
@ -171,7 +182,14 @@ public class AutoMapperProfiles : Profile
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Character).OrderBy(p => p.NormalizedName)))
.ForMember(dest => dest.Editors,
opt =>
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Editor).OrderBy(p => p.NormalizedName)));
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Editor).OrderBy(p => p.NormalizedName)))
.ForMember(dest => dest.Teams,
opt =>
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Team).OrderBy(p => p.NormalizedName)))
.ForMember(dest => dest.Locations,
opt =>
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Location).OrderBy(p => p.NormalizedName)))
;
CreateMap<AppUser, UserDto>()
.ForMember(dest => dest.AgeRestriction,
@ -200,6 +218,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,

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using API.Entities;
using API.Entities.Enums;
using API.Services.Tasks.Scanner.Parser;
@ -17,20 +18,25 @@ public class ChapterBuilder : IEntityBuilder<Chapter>
{
_chapter = new Chapter()
{
Range = string.IsNullOrEmpty(range) ? number : range,
Range = string.IsNullOrEmpty(range) ? number : Parser.RemoveExtensionIfSupported(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
};
}
public static ChapterBuilder FromParserInfo(ParserInfo info)
{
var specialTreatment = info.IsSpecialInfo();
var specialTitle = specialTreatment ? info.Filename : info.Chapters;
var specialTitle = specialTreatment ? Parser.RemoveExtensionIfSupported(info.Filename) : info.Chapters;
var builder = new ChapterBuilder(Parser.DefaultChapter);
return builder.WithNumber(specialTreatment ? Parser.DefaultChapter : Parser.MinNumberFromRange(info.Chapters) + string.Empty)
return builder.WithNumber(Parser.RemoveExtensionIfSupported(info.Chapters))
.WithRange(specialTreatment ? info.Filename : info.Chapters)
.WithTitle((specialTreatment && info.Format == MangaFormat.Epub)
? info.Title
@ -44,9 +50,18 @@ public class ChapterBuilder : IEntityBuilder<Chapter>
return this;
}
public ChapterBuilder WithNumber(string number)
private 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;
}
@ -62,9 +77,9 @@ public class ChapterBuilder : IEntityBuilder<Chapter>
return this;
}
private ChapterBuilder WithRange(string range)
public ChapterBuilder WithRange(string range)
{
_chapter.Range = range;
_chapter.Range = Parser.RemoveExtensionIfSupported(range);
return this;
}

View File

@ -2,6 +2,7 @@
using System.IO;
using API.Entities;
using API.Entities.Enums;
using API.Services.Tasks.Scanner.Parser;
namespace API.Helpers.Builders;
@ -19,6 +20,7 @@ public class MangaFileBuilder : IEntityBuilder<MangaFile>
Pages = pages,
LastModified = File.GetLastWriteTime(filePath),
LastModifiedUtc = File.GetLastWriteTimeUtc(filePath),
FileName = Parser.RemoveExtensionIfSupported(filePath)
};
}

Some files were not shown because too many files have changed in this diff Show More