Small Fixes (#3951)

This commit is contained in:
Joe Milazzo 2025-07-24 13:37:17 -06:00 committed by GitHub
parent 152f7ad00e
commit 032b8f54b7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 459 additions and 285 deletions

View File

@ -10,8 +10,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.1" /> <PackageReference Include="BenchmarkDotNet" Version="0.15.2" />
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.15.1" /> <PackageReference Include="BenchmarkDotNet.Annotations" Version="0.15.2" />
<PackageReference Include="NSubstitute" Version="5.3.0" /> <PackageReference Include="NSubstitute" Version="5.3.0" />
</ItemGroup> </ItemGroup>

View File

@ -6,13 +6,13 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.7" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="NSubstitute" Version="5.3.0" /> <PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="22.0.14" /> <PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="22.0.15" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.0.14" /> <PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.0.15" />
<PackageReference Include="xunit" Version="2.9.3" /> <PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.1"> <PackageReference Include="xunit.runner.visualstudio" Version="3.1.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>

View File

@ -46,8 +46,8 @@ public class DefaultParserTests
[Theory] [Theory]
[InlineData("/manga/Btooom!/Vol.1/Chapter 1/1.cbz", new [] {"Btooom!", "1", "1"})] [InlineData("/manga/Btooom!/Vol.1/Chapter 1/1.cbz", new [] {"Btooom!", "1", "1"})]
[InlineData("/manga/Btooom!/Vol.1 Chapter 2/1.cbz", new [] {"Btooom!", "1", "2"})] [InlineData("/manga/Btooom!/Vol.1 Chapter 2/1.cbz", new [] {"Btooom!", "1", "2"})]
[InlineData("/manga/Monster/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg", new [] {"Monster", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, "1"})] [InlineData("/manga/Monster/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg", new [] {"Monster", Parser.LooseLeafVolume, "1"})]
[InlineData("/manga/Hajime no Ippo/Artbook/Hajime no Ippo - Artbook.cbz", new [] {"Hajime no Ippo", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter})] [InlineData("/manga/Hajime no Ippo/Artbook/Hajime no Ippo - Artbook.cbz", new [] {"Hajime no Ippo", Parser.LooseLeafVolume, Parser.DefaultChapter})]
public void ParseFromFallbackFolders_ShouldParseSeriesVolumeAndChapter(string inputFile, string[] expectedParseInfo) public void ParseFromFallbackFolders_ShouldParseSeriesVolumeAndChapter(string inputFile, string[] expectedParseInfo)
{ {
const string rootDirectory = "/manga/"; const string rootDirectory = "/manga/";
@ -119,7 +119,7 @@ public class DefaultParserTests
expected.Add(filepath, new ParserInfo expected.Add(filepath, new ParserInfo
{ {
Series = "Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen", Volumes = "1", Series = "Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen", Volumes = "1",
Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, Filename = "Vol 1.cbz", Format = MangaFormat.Archive, Chapters = Parser.DefaultChapter, Filename = "Vol 1.cbz", Format = MangaFormat.Archive,
FullFilePath = filepath FullFilePath = filepath
}); });
@ -144,7 +144,7 @@ public class DefaultParserTests
expected.Add(filepath, new ParserInfo expected.Add(filepath, new ParserInfo
{ {
Series = "Tenjo Tenge {Full Contact Edition}", Volumes = "1", Edition = "", Series = "Tenjo Tenge {Full Contact Edition}", Volumes = "1", Edition = "",
Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, Filename = "Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz", Format = MangaFormat.Archive, Chapters = Parser.DefaultChapter, Filename = "Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz", Format = MangaFormat.Archive,
FullFilePath = filepath FullFilePath = filepath
}); });
@ -152,7 +152,7 @@ public class DefaultParserTests
expected.Add(filepath, new ParserInfo expected.Add(filepath, new ParserInfo
{ {
Series = "Akame ga KILL! ZERO", Volumes = "1", Edition = "", Series = "Akame ga KILL! ZERO", Volumes = "1", Edition = "",
Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, Filename = "Akame ga KILL! ZERO v01 (2016) (Digital) (LuCaZ).cbz", Format = MangaFormat.Archive, Chapters = Parser.DefaultChapter, Filename = "Akame ga KILL! ZERO v01 (2016) (Digital) (LuCaZ).cbz", Format = MangaFormat.Archive,
FullFilePath = filepath FullFilePath = filepath
}); });
@ -160,14 +160,14 @@ public class DefaultParserTests
expected.Add(filepath, new ParserInfo expected.Add(filepath, new ParserInfo
{ {
Series = "Dorohedoro", Volumes = "1", Edition = "", Series = "Dorohedoro", Volumes = "1", Edition = "",
Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, Filename = "Dorohedoro v01 (2010) (Digital) (LostNerevarine-Empire).cbz", Format = MangaFormat.Archive, Chapters = Parser.DefaultChapter, Filename = "Dorohedoro v01 (2010) (Digital) (LostNerevarine-Empire).cbz", Format = MangaFormat.Archive,
FullFilePath = filepath FullFilePath = filepath
}); });
filepath = @"E:/Manga/APOSIMZ/APOSIMZ 040 (2020) (Digital) (danke-Empire).cbz"; filepath = @"E:/Manga/APOSIMZ/APOSIMZ 040 (2020) (Digital) (danke-Empire).cbz";
expected.Add(filepath, new ParserInfo expected.Add(filepath, new ParserInfo
{ {
Series = "APOSIMZ", Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, Edition = "", Series = "APOSIMZ", Volumes = Parser.LooseLeafVolume, Edition = "",
Chapters = "40", Filename = "APOSIMZ 040 (2020) (Digital) (danke-Empire).cbz", Format = MangaFormat.Archive, Chapters = "40", Filename = "APOSIMZ 040 (2020) (Digital) (danke-Empire).cbz", Format = MangaFormat.Archive,
FullFilePath = filepath FullFilePath = filepath
}); });
@ -175,7 +175,7 @@ public class DefaultParserTests
filepath = @"E:/Manga/Corpse Party Musume/Kedouin Makoto - Corpse Party Musume, Chapter 09.cbz"; filepath = @"E:/Manga/Corpse Party Musume/Kedouin Makoto - Corpse Party Musume, Chapter 09.cbz";
expected.Add(filepath, new ParserInfo expected.Add(filepath, new ParserInfo
{ {
Series = "Kedouin Makoto - Corpse Party Musume", Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, Edition = "", Series = "Kedouin Makoto - Corpse Party Musume", Volumes = Parser.LooseLeafVolume, Edition = "",
Chapters = "9", Filename = "Kedouin Makoto - Corpse Party Musume, Chapter 09.cbz", Format = MangaFormat.Archive, Chapters = "9", Filename = "Kedouin Makoto - Corpse Party Musume, Chapter 09.cbz", Format = MangaFormat.Archive,
FullFilePath = filepath FullFilePath = filepath
}); });
@ -183,7 +183,7 @@ public class DefaultParserTests
filepath = @"E:/Manga/Goblin Slayer/Goblin Slayer - Brand New Day 006.5 (2019) (Digital) (danke-Empire).cbz"; filepath = @"E:/Manga/Goblin Slayer/Goblin Slayer - Brand New Day 006.5 (2019) (Digital) (danke-Empire).cbz";
expected.Add(filepath, new ParserInfo expected.Add(filepath, new ParserInfo
{ {
Series = "Goblin Slayer - Brand New Day", Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, Edition = "", Series = "Goblin Slayer - Brand New Day", Volumes = Parser.LooseLeafVolume, Edition = "",
Chapters = "6.5", Filename = "Goblin Slayer - Brand New Day 006.5 (2019) (Digital) (danke-Empire).cbz", Format = MangaFormat.Archive, Chapters = "6.5", Filename = "Goblin Slayer - Brand New Day 006.5 (2019) (Digital) (danke-Empire).cbz", Format = MangaFormat.Archive,
FullFilePath = filepath FullFilePath = filepath
}); });
@ -191,15 +191,15 @@ public class DefaultParserTests
filepath = @"E:/Manga/Summer Time Rendering/Specials/Record 014 (between chapter 083 and ch084) SP11.cbr"; filepath = @"E:/Manga/Summer Time Rendering/Specials/Record 014 (between chapter 083 and ch084) SP11.cbr";
expected.Add(filepath, new ParserInfo expected.Add(filepath, new ParserInfo
{ {
Series = "Summer Time Rendering", Volumes = API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume, Edition = "", Series = "Summer Time Rendering", Volumes = Parser.SpecialVolume, Edition = "",
Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, Filename = "Record 014 (between chapter 083 and ch084) SP11.cbr", Format = MangaFormat.Archive, Chapters = Parser.DefaultChapter, Filename = "Record 014 (between chapter 083 and ch084) SP11.cbr", Format = MangaFormat.Archive,
FullFilePath = filepath, IsSpecial = true FullFilePath = filepath, IsSpecial = true
}); });
filepath = @"E:/Manga/Seraph of the End/Seraph of the End - Vampire Reign 093 (2020) (Digital) (LuCaZ).cbz"; filepath = @"E:/Manga/Seraph of the End/Seraph of the End - Vampire Reign 093 (2020) (Digital) (LuCaZ).cbz";
expected.Add(filepath, new ParserInfo expected.Add(filepath, new ParserInfo
{ {
Series = "Seraph of the End - Vampire Reign", Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, Edition = "", Series = "Seraph of the End - Vampire Reign", Volumes = Parser.LooseLeafVolume, Edition = "",
Chapters = "93", Filename = "Seraph of the End - Vampire Reign 093 (2020) (Digital) (LuCaZ).cbz", Format = MangaFormat.Archive, Chapters = "93", Filename = "Seraph of the End - Vampire Reign 093 (2020) (Digital) (LuCaZ).cbz", Format = MangaFormat.Archive,
FullFilePath = filepath, IsSpecial = false FullFilePath = filepath, IsSpecial = false
}); });
@ -227,7 +227,7 @@ public class DefaultParserTests
filepath = @"E:/Manga/The Beginning After the End/Chapter 001.cbz"; filepath = @"E:/Manga/The Beginning After the End/Chapter 001.cbz";
expected.Add(filepath, new ParserInfo expected.Add(filepath, new ParserInfo
{ {
Series = "The Beginning After the End", Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, Edition = "", Series = "The Beginning After the End", Volumes = Parser.LooseLeafVolume, Edition = "",
Chapters = "1", Filename = "Chapter 001.cbz", Format = MangaFormat.Archive, Chapters = "1", Filename = "Chapter 001.cbz", Format = MangaFormat.Archive,
FullFilePath = filepath, IsSpecial = false FullFilePath = filepath, IsSpecial = false
}); });
@ -236,7 +236,7 @@ public class DefaultParserTests
expected.Add(filepath, new ParserInfo expected.Add(filepath, new ParserInfo
{ {
Series = "Air Gear", Volumes = "1", Edition = "Omnibus", Series = "Air Gear", Volumes = "1", Edition = "Omnibus",
Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, Filename = "Air Gear Omnibus v01 (2016) (Digital) (Shadowcat-Empire).cbz", Format = MangaFormat.Archive, Chapters = Parser.DefaultChapter, Filename = "Air Gear Omnibus v01 (2016) (Digital) (Shadowcat-Empire).cbz", Format = MangaFormat.Archive,
FullFilePath = filepath, IsSpecial = false FullFilePath = filepath, IsSpecial = false
}); });
@ -244,7 +244,7 @@ public class DefaultParserTests
expected.Add(filepath, new ParserInfo expected.Add(filepath, new ParserInfo
{ {
Series = "Harrison, Kim - The Good, The Bad, and the Undead - Hollows", Volumes = "2.5", Edition = "", Series = "Harrison, Kim - The Good, The Bad, and the Undead - Hollows", Volumes = "2.5", Edition = "",
Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, Filename = "Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub", Format = MangaFormat.Epub, Chapters = Parser.DefaultChapter, Filename = "Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub", Format = MangaFormat.Epub,
FullFilePath = filepath, IsSpecial = false FullFilePath = filepath, IsSpecial = false
}); });
@ -285,7 +285,7 @@ public class DefaultParserTests
var filepath = @"E:/Manga/Monster #8/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster #8 Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg"; 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 var expectedInfo2 = new ParserInfo
{ {
Series = "Monster #8", Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, Edition = "", Series = "Monster #8", Volumes = Parser.LooseLeafVolume, Edition = "",
Chapters = "8", Filename = "13.jpg", Format = MangaFormat.Image, Chapters = "8", Filename = "13.jpg", Format = MangaFormat.Image,
FullFilePath = filepath, IsSpecial = false FullFilePath = filepath, IsSpecial = false
}; };
@ -407,7 +407,7 @@ public class DefaultParserTests
filepath = @"E:/Manga/Foo 50/Specials/Foo 50 SP01.cbz"; filepath = @"E:/Manga/Foo 50/Specials/Foo 50 SP01.cbz";
expected = new ParserInfo expected = new ParserInfo
{ {
Series = "Foo 50", Volumes = API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume, IsSpecial = true, Series = "Foo 50", Volumes = Parser.SpecialVolume, IsSpecial = true,
Chapters = Parser.DefaultChapter, Filename = "Foo 50 SP01.cbz", Format = MangaFormat.Archive, Chapters = Parser.DefaultChapter, Filename = "Foo 50 SP01.cbz", Format = MangaFormat.Archive,
FullFilePath = filepath FullFilePath = filepath
}; };
@ -442,8 +442,8 @@ public class DefaultParserTests
var filepath = @"E:/Comics/Teen Titans/Teen Titans v1 Annual 01 (1967) SP01.cbr"; var filepath = @"E:/Comics/Teen Titans/Teen Titans v1 Annual 01 (1967) SP01.cbr";
expected.Add(filepath, new ParserInfo expected.Add(filepath, new ParserInfo
{ {
Series = "Teen Titans", Volumes = API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume, Series = "Teen Titans", Volumes = Parser.SpecialVolume,
Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, Filename = "Teen Titans v1 Annual 01 (1967) SP01.cbr", Format = MangaFormat.Archive, Chapters = Parser.DefaultChapter, Filename = "Teen Titans v1 Annual 01 (1967) SP01.cbr", Format = MangaFormat.Archive,
FullFilePath = filepath FullFilePath = filepath
}); });
@ -451,7 +451,7 @@ public class DefaultParserTests
filepath = @"E:/Comics/Comics/Babe/Babe Vol.1 #1-4/Babe 01.cbr"; filepath = @"E:/Comics/Comics/Babe/Babe Vol.1 #1-4/Babe 01.cbr";
expected.Add(filepath, new ParserInfo expected.Add(filepath, new ParserInfo
{ {
Series = "Babe", Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, Edition = "", Series = "Babe", Volumes = Parser.LooseLeafVolume, Edition = "",
Chapters = "1", Filename = "Babe 01.cbr", Format = MangaFormat.Archive, Chapters = "1", Filename = "Babe 01.cbr", Format = MangaFormat.Archive,
FullFilePath = filepath, IsSpecial = false FullFilePath = filepath, IsSpecial = false
}); });
@ -467,7 +467,7 @@ public class DefaultParserTests
filepath = @"E:/Comics/Comics/Batman - The Man Who Laughs #1 (2005)/Batman - The Man Who Laughs #1 (2005).cbr"; filepath = @"E:/Comics/Comics/Batman - The Man Who Laughs #1 (2005)/Batman - The Man Who Laughs #1 (2005).cbr";
expected.Add(filepath, new ParserInfo expected.Add(filepath, new ParserInfo
{ {
Series = "Batman - The Man Who Laughs", Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, Edition = "", Series = "Batman - The Man Who Laughs", Volumes = Parser.LooseLeafVolume, Edition = "",
Chapters = "1", Filename = "Batman - The Man Who Laughs #1 (2005).cbr", Format = MangaFormat.Archive, Chapters = "1", Filename = "Batman - The Man Who Laughs #1 (2005).cbr", Format = MangaFormat.Archive,
FullFilePath = filepath, IsSpecial = false FullFilePath = filepath, IsSpecial = false
}); });

View File

@ -1,4 +1,5 @@
using API.Entities.Enums; using API.Entities.Enums;
using API.Services.Tasks.Scanner.Parser;
using Xunit; using Xunit;
namespace API.Tests.Parsing; namespace API.Tests.Parsing;
@ -17,7 +18,7 @@ public class MangaParsingTests
[InlineData("v001", "1")] [InlineData("v001", "1")]
[InlineData("Vol 1", "1")] [InlineData("Vol 1", "1")]
[InlineData("vol_356-1", "356")] // Mangapy syntax [InlineData("vol_356-1", "356")] // Mangapy syntax
[InlineData("No Volume", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)] [InlineData("No Volume", Parser.LooseLeafVolume)]
[InlineData("U12 (Under 12) Vol. 0001 Ch. 0001 - Reiwa Scans (gb)", "1")] [InlineData("U12 (Under 12) Vol. 0001 Ch. 0001 - Reiwa Scans (gb)", "1")]
[InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip", "1.1")] [InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip", "1.1")]
[InlineData("Tonikaku Cawaii [Volume 11].cbz", "11")] [InlineData("Tonikaku Cawaii [Volume 11].cbz", "11")]
@ -32,18 +33,18 @@ public class MangaParsingTests
[InlineData("Dorohedoro v01 (2010) (Digital) (LostNerevarine-Empire).cbz", "1")] [InlineData("Dorohedoro v01 (2010) (Digital) (LostNerevarine-Empire).cbz", "1")]
[InlineData("Dorohedoro v11 (2013) (Digital) (LostNerevarine-Empire).cbz", "11")] [InlineData("Dorohedoro v11 (2013) (Digital) (LostNerevarine-Empire).cbz", "11")]
[InlineData("Yumekui_Merry_v01_c01[Bakayarou-Kuu].rar", "1")] [InlineData("Yumekui_Merry_v01_c01[Bakayarou-Kuu].rar", "1")]
[InlineData("Yumekui-Merry_DKThias_Chapter11v2.zip", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)] [InlineData("Yumekui-Merry_DKThias_Chapter11v2.zip", Parser.LooseLeafVolume)]
[InlineData("Itoshi no Karin - c001-006x1 (v01) [Renzokusei Scans]", "1")] [InlineData("Itoshi no Karin - c001-006x1 (v01) [Renzokusei Scans]", "1")]
[InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 12", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)] [InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 12", Parser.LooseLeafVolume)]
[InlineData("VanDread-v01-c001[MD].zip", "1")] [InlineData("VanDread-v01-c001[MD].zip", "1")]
[InlineData("Ichiban_Ushiro_no_Daimaou_v04_ch27_[VISCANS].zip", "4")] [InlineData("Ichiban_Ushiro_no_Daimaou_v04_ch27_[VISCANS].zip", "4")]
[InlineData("Mob Psycho 100 v02 (2019) (Digital) (Shizu).cbz", "2")] [InlineData("Mob Psycho 100 v02 (2019) (Digital) (Shizu).cbz", "2")]
[InlineData("Kodomo no Jikan vol. 1.cbz", "1")] [InlineData("Kodomo no Jikan vol. 1.cbz", "1")]
[InlineData("Kodomo no Jikan vol. 10.cbz", "10")] [InlineData("Kodomo no Jikan vol. 10.cbz", "10")]
[InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 12 [Dametrans][v2]", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)] [InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 12 [Dametrans][v2]", Parser.LooseLeafVolume)]
[InlineData("Vagabond_v03", "3")] [InlineData("Vagabond_v03", "3")]
[InlineData("Mujaki No Rakune Volume 10.cbz", "10")] [InlineData("Mujaki No Rakune Volume 10.cbz", "10")]
[InlineData("Umineko no Naku Koro ni - Episode 3 - Banquet of the Golden Witch #02.cbz", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)] [InlineData("Umineko no Naku Koro ni - Episode 3 - Banquet of the Golden Witch #02.cbz", Parser.LooseLeafVolume)]
[InlineData("Volume 12 - Janken Boy is Coming!.cbz", "12")] [InlineData("Volume 12 - Janken Boy is Coming!.cbz", "12")]
[InlineData("[dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz", "20")] [InlineData("[dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz", "20")]
[InlineData("Gantz.V26.cbz", "26")] [InlineData("Gantz.V26.cbz", "26")]
@ -52,7 +53,7 @@ public class MangaParsingTests
[InlineData("NEEDLESS_Vol.4_-_Simeon_6_v2_[SugoiSugoi].rar", "4")] [InlineData("NEEDLESS_Vol.4_-_Simeon_6_v2_[SugoiSugoi].rar", "4")]
[InlineData("Okusama wa Shougakusei c003 (v01) [bokuwaNEET]", "1")] [InlineData("Okusama wa Shougakusei c003 (v01) [bokuwaNEET]", "1")]
[InlineData("Sword Art Online Vol 10 - Alicization Running [Yen Press] [LuCaZ] {r2}.epub", "10")] [InlineData("Sword Art Online Vol 10 - Alicization Running [Yen Press] [LuCaZ] {r2}.epub", "10")]
[InlineData("Noblesse - Episode 406 (52 Pages).7z", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)] [InlineData("Noblesse - Episode 406 (52 Pages).7z", Parser.LooseLeafVolume)]
[InlineData("X-Men v1 #201 (September 2007).cbz", "1")] [InlineData("X-Men v1 #201 (September 2007).cbz", "1")]
[InlineData("Hentai Ouji to Warawanai Neko. - Vol. 06 Ch. 034.5", "6")] [InlineData("Hentai Ouji to Warawanai Neko. - Vol. 06 Ch. 034.5", "6")]
[InlineData("The 100 Girlfriends Who Really, Really, Really, Really, Really Love You - Vol. 03 Ch. 023.5 - Volume 3 Extras.cbz", "3")] [InlineData("The 100 Girlfriends Who Really, Really, Really, Really, Really Love You - Vol. 03 Ch. 023.5 - Volume 3 Extras.cbz", "3")]
@ -64,7 +65,7 @@ public class MangaParsingTests
[InlineData("スライム倒して300年、知らないうちにレベルMAXになってました 1-3巻", "1-3")] [InlineData("スライム倒して300年、知らないうちにレベルMAXになってました 1-3巻", "1-3")]
[InlineData("Dance in the Vampire Bund {Special Edition} v03.5 (2019) (Digital) (KG Manga)", "3.5")] [InlineData("Dance in the Vampire Bund {Special Edition} v03.5 (2019) (Digital) (KG Manga)", "3.5")]
[InlineData("Kebab Том 1 Глава 3", "1")] [InlineData("Kebab Том 1 Глава 3", "1")]
[InlineData("Манга Глава 2", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)] [InlineData("Манга Глава 2", Parser.LooseLeafVolume)]
[InlineData("Манга Тома 1-4", "1-4")] [InlineData("Манга Тома 1-4", "1-4")]
[InlineData("Манга Том 1-4", "1-4")] [InlineData("Манга Том 1-4", "1-4")]
[InlineData("조선왕조실톡 106화", "106")] [InlineData("조선왕조실톡 106화", "106")]
@ -76,9 +77,19 @@ public class MangaParsingTests
[InlineData("Accel World Volume 2", "2")] [InlineData("Accel World Volume 2", "2")]
[InlineData("Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.31 Omake", "30")] [InlineData("Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.31 Omake", "30")]
[InlineData("Zom 100 - Bucket List of the Dead v01", "1")] [InlineData("Zom 100 - Bucket List of the Dead v01", "1")]
// Tome Tests
[InlineData("Daredevil - t6 - 10 - (2019)", "6")]
[InlineData("Batgirl T2000 #57", "2000")]
[InlineData("Teen Titans t1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)", "1")]
[InlineData("Conquistador_Tome_2", "2")]
[InlineData("Max_l_explorateur-_Tome_0", "0")]
[InlineData("Chevaliers d'Héliopolis T3 - Rubedo, l'oeuvre au rouge (Jodorowsky & Jérémy)", "3")]
[InlineData("Adventure Time (2012)/Adventure Time Ch 1 (2012)", Parser.LooseLeafVolume)]
[InlineData("Adventure Time TPB (2012)/Adventure Time v01 (2012).cbz", "1")]
[InlineData("Monster Ch. 001 [MangaPlus] [Digital] [amit34521]", Parser.LooseLeafVolume)]
public void ParseVolumeTest(string filename, string expected) public void ParseVolumeTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename, LibraryType.Manga)); Assert.Equal(expected, Parser.ParseVolume(filename, LibraryType.Manga));
} }
[Theory] [Theory]
@ -206,21 +217,26 @@ public class MangaParsingTests
[InlineData("[218565]-(C92) [BRIO (Puyocha)] Mika-nee no Tanryoku Shidou - Mika s Guide to Self-Confidence (THE IDOLM@STE", "")] [InlineData("[218565]-(C92) [BRIO (Puyocha)] Mika-nee no Tanryoku Shidou - Mika s Guide to Self-Confidence (THE IDOLM@STE", "")]
[InlineData("Monster #8 Ch. 001", "Monster #8")] [InlineData("Monster #8 Ch. 001", "Monster #8")]
[InlineData("Zom 100 - Bucket List of the Dead v01", "Zom 100 - Bucket List of the Dead")] [InlineData("Zom 100 - Bucket List of the Dead v01", "Zom 100 - Bucket List of the Dead")]
[InlineData("Zom 100 - Tome 2", "Zom 100")]
[InlineData("Max_l_explorateur Tome 0", "Max l explorateur")]
[InlineData("Chevaliers d'Héliopolis T3 - Rubedo, l'oeuvre au rouge (Jodorowsky & Jérémy)", "Chevaliers d'Héliopolis")]
[InlineData("Bd Fr-Aldebaran-Antares-t6", "Bd Fr-Aldebaran-Antares")]
[InlineData("Monster Ch. 001 [MangaPlus] [Digital] [amit34521]", "Monster")]
public void ParseSeriesTest(string filename, string expected) public void ParseSeriesTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename, LibraryType.Manga)); Assert.Equal(expected, Parser.ParseSeries(filename, LibraryType.Manga));
} }
[Theory] [Theory]
[InlineData("Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)", "1")] [InlineData("Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)", "1")]
[InlineData("My Girlfriend Is Shobitch v01 - ch. 09 - pg. 008.png", "9")] [InlineData("My Girlfriend Is Shobitch v01 - ch. 09 - pg. 008.png", "9")]
[InlineData("Historys Strongest Disciple Kenichi_v11_c90-98.zip", "90-98")] [InlineData("Historys Strongest Disciple Kenichi_v11_c90-98.zip", "90-98")]
[InlineData("B_Gata_H_Kei_v01[SlowManga&OverloadScans]", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] [InlineData("B_Gata_H_Kei_v01[SlowManga&OverloadScans]", Parser.DefaultChapter)]
[InlineData("BTOOOM! v01 (2013) (Digital) (Shadowcat-Empire)", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] [InlineData("BTOOOM! v01 (2013) (Digital) (Shadowcat-Empire)", Parser.DefaultChapter)]
[InlineData("Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA]", "1-8")] [InlineData("Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA]", "1-8")]
[InlineData("Dance in the Vampire Bund v16-17 (Digital) (NiceDragon)", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] [InlineData("Dance in the Vampire Bund v16-17 (Digital) (NiceDragon)", Parser.DefaultChapter)]
[InlineData("c001", "1")] [InlineData("c001", "1")]
[InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.12.zip", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] [InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.12.zip", Parser.DefaultChapter)]
[InlineData("Adding volume 1 with File: Ana Satsujin Vol. 1 Ch. 5 - Manga Box (gb).cbz", "5")] [InlineData("Adding volume 1 with File: Ana Satsujin Vol. 1 Ch. 5 - Manga Box (gb).cbz", "5")]
[InlineData("Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz", "18")] [InlineData("Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz", "18")]
[InlineData("Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip", "0-6")] [InlineData("Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip", "0-6")]
@ -243,7 +259,7 @@ public class MangaParsingTests
[InlineData("Itoshi no Karin - c001-006x1 (v01) [Renzokusei Scans]", "1-6")] [InlineData("Itoshi no Karin - c001-006x1 (v01) [Renzokusei Scans]", "1-6")]
[InlineData("APOSIMZ 040 (2020) (Digital) (danke-Empire).cbz", "40")] [InlineData("APOSIMZ 040 (2020) (Digital) (danke-Empire).cbz", "40")]
[InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 12", "12")] [InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 12", "12")]
[InlineData("Vol 1", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] [InlineData("Vol 1", Parser.DefaultChapter)]
[InlineData("VanDread-v01-c001[MD].zip", "1")] [InlineData("VanDread-v01-c001[MD].zip", "1")]
[InlineData("Goblin Slayer Side Story - Year One 025.5", "25.5")] [InlineData("Goblin Slayer Side Story - Year One 025.5", "25.5")]
[InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 01", "1")] [InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 01", "1")]
@ -255,10 +271,10 @@ public class MangaParsingTests
[InlineData("Fullmetal Alchemist chapters 101-108.cbz", "101-108")] [InlineData("Fullmetal Alchemist chapters 101-108.cbz", "101-108")]
[InlineData("Umineko no Naku Koro ni - Episode 3 - Banquet of the Golden Witch #02.cbz", "2")] [InlineData("Umineko no Naku Koro ni - Episode 3 - Banquet of the Golden Witch #02.cbz", "2")]
[InlineData("To Love Ru v09 Uncensored (Ch.071-079).cbz", "71-79")] [InlineData("To Love Ru v09 Uncensored (Ch.071-079).cbz", "71-79")]
[InlineData("Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Extra Chapter.rar", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] [InlineData("Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Extra Chapter.rar", Parser.DefaultChapter)]
[InlineData("Beelzebub_153b_RHS.zip", "153.5")] [InlineData("Beelzebub_153b_RHS.zip", "153.5")]
[InlineData("Beelzebub_150-153b_RHS.zip", "150-153.5")] [InlineData("Beelzebub_150-153b_RHS.zip", "150-153.5")]
[InlineData("Transferred to another world magical swordsman v1.1", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] [InlineData("Transferred to another world magical swordsman v1.1", Parser.DefaultChapter)]
[InlineData("Kiss x Sis - Ch.15 - The Angst of a 15 Year Old Boy.cbz", "15")] [InlineData("Kiss x Sis - Ch.15 - The Angst of a 15 Year Old Boy.cbz", "15")]
[InlineData("Kiss x Sis - Ch.12 - 1 , 2 , 3P!.cbz", "12")] [InlineData("Kiss x Sis - Ch.12 - 1 , 2 , 3P!.cbz", "12")]
[InlineData("Umineko no Naku Koro ni - Episode 1 - Legend of the Golden Witch #1", "1")] [InlineData("Umineko no Naku Koro ni - Episode 1 - Legend of the Golden Witch #1", "1")]
@ -277,21 +293,21 @@ public class MangaParsingTests
[InlineData("Kimi no Koto ga Daidaidaidaidaisuki na 100-nin no Kanojo Chapter 1-10", "1-10")] [InlineData("Kimi no Koto ga Daidaidaidaidaisuki na 100-nin no Kanojo Chapter 1-10", "1-10")]
[InlineData("Deku_&_Bakugo_-_Rising_v1_c1.1.cbz", "1.1")] [InlineData("Deku_&_Bakugo_-_Rising_v1_c1.1.cbz", "1.1")]
[InlineData("Chapter 63 - The Promise Made for 520 Cenz.cbr", "63")] [InlineData("Chapter 63 - The Promise Made for 520 Cenz.cbr", "63")]
[InlineData("Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] [InlineData("Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub", Parser.DefaultChapter)]
[InlineData("Kaiju No. 8 036 (2021) (Digital)", "36")] [InlineData("Kaiju No. 8 036 (2021) (Digital)", "36")]
[InlineData("Samurai Jack Vol. 01 - The threads of Time", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] [InlineData("Samurai Jack Vol. 01 - The threads of Time", Parser.DefaultChapter)]
[InlineData("【TFO汉化&Petit汉化】迷你偶像漫画第25话", "25")] [InlineData("【TFO汉化&Petit汉化】迷你偶像漫画第25话", "25")]
[InlineData("자유록 13회#2", "13")] [InlineData("자유록 13회#2", "13")]
[InlineData("이세계에서 고아원을 열었지만, 어째서인지 아무도 독립하려 하지 않는다 38-1화 ", "38")] [InlineData("이세계에서 고아원을 열었지만, 어째서인지 아무도 독립하려 하지 않는다 38-1화 ", "38")]
[InlineData("[ハレム] SMごっこ 10", "10")] [InlineData("[ハレム] SMごっこ 10", "10")]
[InlineData("Dance in the Vampire Bund {Special Edition} v03.5 (2019) (Digital) (KG Manga)", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] [InlineData("Dance in the Vampire Bund {Special Edition} v03.5 (2019) (Digital) (KG Manga)", Parser.DefaultChapter)]
[InlineData("Kebab Том 1 Глава 3", "3")] [InlineData("Kebab Том 1 Глава 3", "3")]
[InlineData("Манга Глава 2", "2")] [InlineData("Манга Глава 2", "2")]
[InlineData("Манга 2 Глава", "2")] [InlineData("Манга 2 Глава", "2")]
[InlineData("Манга Том 1 2 Глава", "2")] [InlineData("Манга Том 1 2 Глава", "2")]
[InlineData("Accel World Chapter 001 Volume 002", "1")] [InlineData("Accel World Chapter 001 Volume 002", "1")]
[InlineData("Bleach 001-003", "1-3")] [InlineData("Bleach 001-003", "1-3")]
[InlineData("Accel World Volume 2", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] [InlineData("Accel World Volume 2", Parser.DefaultChapter)]
[InlineData("Historys Strongest Disciple Kenichi_v11_c90-98", "90-98")] [InlineData("Historys Strongest Disciple Kenichi_v11_c90-98", "90-98")]
[InlineData("Historys Strongest Disciple Kenichi c01-c04", "1-4")] [InlineData("Historys Strongest Disciple Kenichi c01-c04", "1-4")]
[InlineData("Adabana c00-02", "0-2")] [InlineData("Adabana c00-02", "0-2")]
@ -299,9 +315,10 @@ public class MangaParsingTests
[InlineData("Max Level Returner ตอนที่ 5", "5")] [InlineData("Max Level Returner ตอนที่ 5", "5")]
[InlineData("หนึ่งความคิด นิจนิรันดร์ บทที่ 112", "112")] [InlineData("หนึ่งความคิด นิจนิรันดร์ บทที่ 112", "112")]
[InlineData("Monster #8 Ch. 001", "1")] [InlineData("Monster #8 Ch. 001", "1")]
[InlineData("Monster Ch. 001 [MangaPlus] [Digital] [amit34521]", "1")]
public void ParseChaptersTest(string filename, string expected) public void ParseChaptersTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename, LibraryType.Manga)); Assert.Equal(expected, Parser.ParseChapter(filename, LibraryType.Manga));
} }
@ -318,8 +335,9 @@ public class MangaParsingTests
[InlineData("Love Hina Omnibus v05 (2015) (Digital-HD) (Asgard-Empire).cbz", "Omnibus")] [InlineData("Love Hina Omnibus v05 (2015) (Digital-HD) (Asgard-Empire).cbz", "Omnibus")]
public void ParseEditionTest(string input, string expected) public void ParseEditionTest(string input, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseEdition(input)); Assert.Equal(expected, Parser.ParseEdition(input));
} }
[Theory] [Theory]
[InlineData("Beelzebub Special OneShot - Minna no Kochikame x Beelzebub (2016) [Mangastream].cbz", false)] [InlineData("Beelzebub Special OneShot - Minna no Kochikame x Beelzebub (2016) [Mangastream].cbz", false)]
[InlineData("Beelzebub_Omake_June_2012_RHS", false)] [InlineData("Beelzebub_Omake_June_2012_RHS", false)]
@ -339,7 +357,7 @@ public class MangaParsingTests
[InlineData("Hajime no Ippo - Artbook", false)] [InlineData("Hajime no Ippo - Artbook", false)]
public void IsMangaSpecialTest(string input, bool expected) public void IsMangaSpecialTest(string input, bool expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.IsSpecial(input, LibraryType.Manga)); Assert.Equal(expected, Parser.IsSpecial(input, LibraryType.Manga));
} }
[Theory] [Theory]
@ -348,7 +366,7 @@ public class MangaParsingTests
[InlineData("image.txt", MangaFormat.Unknown)] [InlineData("image.txt", MangaFormat.Unknown)]
public void ParseFormatTest(string inputFile, MangaFormat expected) public void ParseFormatTest(string inputFile, MangaFormat expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseFormat(inputFile)); Assert.Equal(expected, Parser.ParseFormat(inputFile));
} }

View File

@ -51,8 +51,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="CsvHelper" Version="33.1.0" /> <PackageReference Include="CsvHelper" Version="33.1.0" />
<PackageReference Include="MailKit" Version="4.12.1" /> <PackageReference Include="MailKit" Version="4.13.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
@ -66,20 +66,20 @@
<PackageReference Include="Hangfire.InMemory" Version="1.0.0" /> <PackageReference Include="Hangfire.InMemory" Version="1.0.0" />
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" /> <PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.4.2" /> <PackageReference Include="Hangfire.Storage.SQLite" Version="0.4.2" />
<PackageReference Include="HtmlAgilityPack" Version="1.12.1" /> <PackageReference Include="HtmlAgilityPack" Version="1.12.2" />
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" /> <PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.20" /> <PackageReference Include="Hangfire.AspNetCore" Version="1.8.20" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" /> <PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.6" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.6" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.7" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" /> <PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" /> <PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
<PackageReference Include="Nager.ArticleNumber" Version="1.0.7" /> <PackageReference Include="Nager.ArticleNumber" Version="1.0.7" />
<PackageReference Include="NetVips" Version="3.1.0" /> <PackageReference Include="NetVips" Version="3.1.0" />
<PackageReference Include="NetVips.Native" Version="8.17.0.1" /> <PackageReference Include="NetVips.Native" Version="8.17.1" />
<PackageReference Include="Serilog" Version="4.3.0" /> <PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" /> <PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
@ -91,15 +91,15 @@
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" /> <PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
<PackageReference Include="SharpCompress" Version="0.40.0" /> <PackageReference Include="SharpCompress" Version="0.40.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.11.0.117924"> <PackageReference Include="SonarAnalyzer.CSharp" Version="10.15.0.120848">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.3" /> <PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="9.0.0" />
<PackageReference Include="System.Drawing.Common" Version="9.0.6" /> <PackageReference Include="System.Drawing.Common" Version="9.0.7" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.12.0" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.13.0" />
<PackageReference Include="System.IO.Abstractions" Version="22.0.14" /> <PackageReference Include="System.IO.Abstractions" Version="22.0.15" />
<PackageReference Include="VersOne.Epub" Version="3.3.4" /> <PackageReference Include="VersOne.Epub" Version="3.3.4" />
<PackageReference Include="YamlDotNet" Version="16.3.0" /> <PackageReference Include="YamlDotNet" Version="16.3.0" />
</ItemGroup> </ItemGroup>

View File

@ -16,6 +16,7 @@ using API.Extensions;
using API.Helpers.Builders; using API.Helpers.Builders;
using API.Services; using API.Services;
using API.Services.Tasks.Scanner; using API.Services.Tasks.Scanner;
using API.Services.Tasks.Scanner.Parser;
using API.SignalR; using API.SignalR;
using AutoMapper; using AutoMapper;
using EasyCaching.Core; using EasyCaching.Core;
@ -83,6 +84,7 @@ public class LibraryController : BaseApiController
.WithManageReadingLists(dto.ManageReadingLists) .WithManageReadingLists(dto.ManageReadingLists)
.WithAllowScrobbling(dto.AllowScrobbling) .WithAllowScrobbling(dto.AllowScrobbling)
.WithAllowMetadataMatching(dto.AllowMetadataMatching) .WithAllowMetadataMatching(dto.AllowMetadataMatching)
.WithEnableMetadata(dto.EnableMetadata)
.Build(); .Build();
library.LibraryFileTypes = dto.FileGroupTypes library.LibraryFileTypes = dto.FileGroupTypes
@ -173,6 +175,26 @@ public class LibraryController : BaseApiController
return Ok(_directoryService.ListDirectory(path)); return Ok(_directoryService.ListDirectory(path));
} }
/// <summary>
/// For each root, checks if there are any supported files at root to warn the user during library creation about an invalid setup
/// </summary>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("has-files-at-root")]
public ActionResult<IDictionary<string, bool>> AnyFilesAtRoot(CheckForFilesInFolderRootsDto dto)
{
var results = new Dictionary<string, bool>();
foreach (var root in dto.Roots)
{
results.TryAdd(root,
_directoryService
.GetFilesWithCertainExtensions(root, Parser.SupportedExtensions, SearchOption.TopDirectoryOnly)
.Any());
}
return Ok(results);
}
/// <summary> /// <summary>
/// Return a specific library /// Return a specific library
/// </summary> /// </summary>

View File

@ -148,6 +148,18 @@ public class PersonController : BaseApiController
return Ok(_mapper.Map<PersonDto>(person)); return Ok(_mapper.Map<PersonDto>(person));
} }
/// <summary>
/// Validates if the ASIN (10/13) is valid
/// </summary>
/// <param name="asin"></param>
/// <returns></returns>
[HttpGet("valid-asin")]
public ActionResult<bool> ValidateAsin(string asin)
{
return Ok(!string.IsNullOrEmpty(asin) &&
(ArticleNumberHelper.IsValidIsbn10(asin) || ArticleNumberHelper.IsValidIsbn13(asin)));
}
/// <summary> /// <summary>
/// Attempts to download the cover from CoversDB (Note: Not yet release in Kavita) /// Attempts to download the cover from CoversDB (Note: Not yet release in Kavita)
/// </summary> /// </summary>

View File

@ -0,0 +1,8 @@
using System.Collections.Generic;
namespace API.DTOs;
public sealed record CheckForFilesInFolderRootsDto
{
public ICollection<string> Roots { get; init; }
}

View File

@ -130,7 +130,7 @@ public class PdfComicInfoExtractor : IPdfComicInfoExtractor
{ {
try try
{ {
var extractor = new PdfMetadataExtractor(_logger, filePath); using var extractor = new PdfMetadataExtractor(_logger, filePath);
return GetComicInfoFromMetadata(extractor.GetMetadata(), filePath); return GetComicInfoFromMetadata(extractor.GetMetadata(), filePath);
} }
@ -138,9 +138,12 @@ public class PdfComicInfoExtractor : IPdfComicInfoExtractor
{ {
_logger.LogWarning(ex, "[GetComicInfo] There was an exception parsing PDF metadata for {File}", filePath); _logger.LogWarning(ex, "[GetComicInfo] There was an exception parsing PDF metadata for {File}", filePath);
_mediaErrorService.ReportMediaIssue(filePath, MediaErrorProducer.BookService, _mediaErrorService.ReportMediaIssue(filePath, MediaErrorProducer.BookService,
"There was an exception parsing PDF metadata", ex); ex.Message == "Encryption not supported"
? "Encrypted PDFs are not supported"
: "There was an exception parsing PDF metadata", ex);
} }
return null; return null;
} }
} }

View File

@ -1,23 +1,51 @@
/**
* Contributed by https://github.com/microtherion
*
* All references to the "PDF Spec" (section numbers, etc) refer to the
* PDF 1.7 Specification a.k.a. PDF32000-1:2008
* https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf
*/
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO.Compression; using System.IO.Compression;
using System.Text; using System.Text;
using System.Xml; using System.Xml;
using System.IO; using System.IO;
using Microsoft.Extensions.Logging;
using API.Services; using API.Services;
using Microsoft.Extensions.Logging;
namespace API.Helpers; namespace API.Helpers;
#nullable enable #nullable enable
/**
* Contributed by https://github.com/microtherion
*
* All references to the "PDF Spec" (section numbers, etc.) refer to the
* PDF 1.7 Specification a.k.a. PDF32000-1:2008
* https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf
*/
/**
* Reference for PDF Metadata Format
%PDF-1.4 Header
Object 1 0 obj Objects containing content
<< /Type /Catalog ... >>
endobj
Object 2 0 obj
<< /Type /Info ... >>
endobj
...more objects...
xref Cross-reference table
0 6
0000000000 65535 f
0000000015 00000 n Object 1 is at byte offset 15
0000000109 00000 n Object 2 is at byte offset 109
...
trailer Trailer dictionary
<< /Size 6 /Root 1 0 R /Info 2 0 R >>
startxref
1234 Byte offset where xref starts
%%EOF
*/
/// <summary> /// <summary>
/// Parse PDF file and try to extract as much metadata as possible. /// Parse PDF file and try to extract as much metadata as possible.
/// Supports both text based XRef tables and compressed XRef streams (Deflate only). /// Supports both text based XRef tables and compressed XRef streams (Deflate only).
@ -41,17 +69,17 @@ public class PdfMetadataExtractorException : Exception
} }
} }
public interface IPdfMetadataExtractor public interface IPdfMetadataExtractor : IDisposable
{ {
Dictionary<String, String> GetMetadata(); Dictionary<string, string> GetMetadata();
} }
class PdfStringBuilder internal class PdfStringBuilder
{ {
private readonly StringBuilder _builder = new(); private readonly StringBuilder _builder = new();
private bool _secondByte = false; private bool _secondByte;
private byte _prevByte = 0; private byte _prevByte;
private bool _isUnicode = false; private bool _isUnicode;
// PDFDocEncoding defined in PDF Spec D.1 // PDFDocEncoding defined in PDF Spec D.1
@ -71,11 +99,11 @@ class PdfStringBuilder
private void AppendPdfDocByte(byte b) private void AppendPdfDocByte(byte b)
{ {
if (b >= 0x18 && b < 0x20) if (b is >= 0x18 and < 0x20)
{ {
_builder.Append(_pdfDocMappingLow[b - 0x18]); _builder.Append(_pdfDocMappingLow[b - 0x18]);
} }
else if (b >= 0x80 && b < 0xA1) else if (b is >= 0x80 and < 0xA1)
{ {
_builder.Append(_pdfDocMappingHigh[b - 0x80]); _builder.Append(_pdfDocMappingHigh[b - 0x80]);
} }
@ -94,29 +122,25 @@ class PdfStringBuilder
{ {
// PDF Spec 7.9.2.1: Strings are either UTF-16BE or PDFDocEncoded // PDF Spec 7.9.2.1: Strings are either UTF-16BE or PDFDocEncoded
if (_builder.Length == 0 && !_isUnicode) if (_builder.Length == 0 && !_isUnicode)
{
switch (_secondByte)
{ {
// Unicode strings are prefixed by a big endian BOM \uFEFF // Unicode strings are prefixed by a big endian BOM \uFEFF
if (_secondByte) case true when b == 0xFF:
{
if (b == 0xFF)
{
_isUnicode = true; _isUnicode = true;
_secondByte = false; _secondByte = false;
} break;
else case true:
{
AppendPdfDocByte(_prevByte); AppendPdfDocByte(_prevByte);
AppendPdfDocByte(b); AppendPdfDocByte(b);
} break;
} case false when b == 0xFE:
else if (!_secondByte && b == 0xFE)
{
_secondByte = true; _secondByte = true;
_prevByte = b; _prevByte = b;
} break;
else default:
{
AppendPdfDocByte(b); AppendPdfDocByte(b);
break;
} }
} }
else if (_isUnicode) else if (_isUnicode)
@ -138,7 +162,7 @@ class PdfStringBuilder
} }
} }
override public string ToString() public override string ToString()
{ {
if (_builder.Length == 0 && _secondByte) if (_builder.Length == 0 && _secondByte)
{ {
@ -153,8 +177,8 @@ internal class PdfLexer(Stream stream)
{ {
private const int BufferSize = 1024; private const int BufferSize = 1024;
private readonly byte[] _buffer = new byte[BufferSize]; private readonly byte[] _buffer = new byte[BufferSize];
private int _pos = 0; private int _pos;
private int _valid = 0; private int _valid;
public enum TokenType public enum TokenType
{ {
@ -353,11 +377,9 @@ internal class PdfLexer(Stream stream)
{ {
return (long)token.Value; return (long)token.Value;
} }
else
{
throw new PdfMetadataExtractorException("Expected integer after startxref keyword"); throw new PdfMetadataExtractorException("Expected integer after startxref keyword");
} }
}
continue; continue;
@ -367,10 +389,18 @@ internal class PdfLexer(Stream stream)
} }
} }
public bool NextXRefEntry(ref long obj, ref int generation) /// <summary>
///
/// </summary>
/// <example>
/// 0000000015 00000 n ← offset=15, generation=0, in-use
/// 0000000109 00000 n ← offset=109, generation=0, in-use
/// 0000000000 65535 f ← offset=0, generation=65535, free
/// </example>
/// <remarks>Cross-reference table entry as per PDF Spec 7.5.4</remarks>
/// <exception cref="PdfMetadataExtractorException"></exception>
public bool NextXRefEntry(out long offset, out int generation)
{ {
// Cross-reference table entry as per PDF Spec 7.5.4
WantLookahead(20); WantLookahead(20);
if (_valid - _pos < 20) if (_valid - _pos < 20)
@ -378,14 +408,11 @@ internal class PdfLexer(Stream stream)
throw new PdfMetadataExtractorException("End of stream"); throw new PdfMetadataExtractorException("End of stream");
} }
var inUse = true; // Parse the 20-byte XRef entry: "nnnnnnnnnn ggggg n/f \r\n"
offset = Convert.ToInt64(Encoding.ASCII.GetString(_buffer, _pos, 10).Trim());
generation = Convert.ToInt32(Encoding.ASCII.GetString(_buffer, _pos + 11, 5).Trim());
if (obj == 0) var inUse = _buffer[_pos + 17] == 'n';
{
obj = Convert.ToInt64(Encoding.ASCII.GetString(_buffer, _pos, 10));
generation = Convert.ToInt32(Encoding.ASCII.GetString(_buffer, _pos + 11, 5));
inUse = _buffer[_pos + 17] == 'n';
}
_pos += 20; _pos += 20;
@ -503,7 +530,7 @@ internal class PdfLexer(Stream stream)
{ {
StringBuilder sb = new(); StringBuilder sb = new();
var hasDot = LastByte() == '.'; var hasDot = LastByte() == '.';
var followedBySpace = false; bool followedBySpace;
sb.Append((char)LastByte()); sb.Append((char)LastByte());
@ -647,7 +674,8 @@ internal class PdfLexer(Stream stream)
case '(': case '(':
parenLevel++; parenLevel++;
goto default; sb.AppendByte(b);
break;
case ')': case ')':
if (--parenLevel == 0) if (--parenLevel == 0)
@ -655,7 +683,8 @@ internal class PdfLexer(Stream stream)
return new Token(TokenType.String, sb.ToString()); return new Token(TokenType.String, sb.ToString());
} }
goto default; sb.AppendByte(b);
break;
case '\\': case '\\':
b = NextByte(); b = NextByte();
@ -688,7 +717,6 @@ internal class PdfLexer(Stream stream)
break; break;
case >= '0' and <= '7': case >= '0' and <= '7':
var b1 = b;
var b2 = NextByte(); var b2 = NextByte();
var b3 = NextByte(); var b3 = NextByte();
@ -697,7 +725,7 @@ internal class PdfLexer(Stream stream)
throw new PdfMetadataExtractorException("Invalid octal escape, got {b1}{b2}{b3}"); throw new PdfMetadataExtractorException("Invalid octal escape, got {b1}{b2}{b3}");
} }
sb.AppendByte((byte)((b1 - '0') << 6 | (b2 - '0') << 3 | (b3 - '0'))); sb.AppendByte((byte)((b - '0') << 6 | (b2 - '0') << 3 | (b3 - '0')));
break; break;
} }
@ -763,26 +791,15 @@ internal class PdfLexer(Stream stream)
} }
} }
switch (sb.ToString()) return sb.ToString() switch
{ {
case "true": "true" => new Token(TokenType.Bool, true),
return new Token(TokenType.Bool, true); "false" => new Token(TokenType.Bool, false),
"stream" => new Token(TokenType.StreamStart, true),
case "false": "endstream" => new Token(TokenType.StreamEnd, true),
return new Token(TokenType.Bool, false); "endobj" => new Token(TokenType.ObjectEnd, true),
_ => new Token(TokenType.Keyword, sb.ToString())
case "stream": };
return new Token(TokenType.StreamStart, true);
case "endstream":
return new Token(TokenType.StreamEnd, true);
case "endobj":
return new Token(TokenType.ObjectEnd, true);
default:
return new Token(TokenType.Keyword, sb.ToString());
}
} }
} }
@ -791,9 +808,10 @@ internal class PdfMetadataExtractor : IPdfMetadataExtractor
private readonly ILogger<BookService> _logger; private readonly ILogger<BookService> _logger;
private readonly PdfLexer _lexer; private readonly PdfLexer _lexer;
private readonly FileStream _stream; private readonly FileStream _stream;
private long[] _objectOffsets = new long[0]; private readonly Dictionary<long, long> _objectOffsets = [];
private readonly Dictionary<string, string> _metadata = []; private readonly Dictionary<string, string> _metadata = [];
private readonly Stack<MetadataRef> _metadataRef = new(); private readonly Stack<MetadataRef> _metadataRef = new();
private bool _disposed;
private struct MetadataRef(long root, long info) private struct MetadataRef(long root, long info)
{ {
@ -801,7 +819,7 @@ internal class PdfMetadataExtractor : IPdfMetadataExtractor
public long Info = info; public long Info = info;
} }
private struct XRefSection(long first, long count) private readonly struct XRefSection(long first, long count)
{ {
public readonly long First = first; public readonly long First = first;
public readonly long Count = count; public readonly long Count = count;
@ -822,7 +840,9 @@ internal class PdfMetadataExtractor : IPdfMetadataExtractor
return _metadata; return _metadata;
} }
#pragma warning disable S1144
private void LogMetadata(string filename) private void LogMetadata(string filename)
#pragma warning restore S1144
{ {
_logger.LogTrace("Metadata for {Path}:", filename); _logger.LogTrace("Metadata for {Path}:", filename);
@ -854,14 +874,11 @@ internal class PdfMetadataExtractor : IPdfMetadataExtractor
if (!_lexer.TestByte((byte)'x')) if (!_lexer.TestByte((byte)'x'))
{ {
// Cross-reference stream (PDF Spec 7.5.8) // Cross-reference stream (PDF Spec 7.5.8)
ReadXRefStream(); ReadXRefStream();
return; return;
} }
// Cross-reference table (PDF Spec 7.5.4) // Cross-reference table (PDF Spec 7.5.4)
var token = _lexer.NextToken(); var token = _lexer.NextToken();
if (token.Type != PdfLexer.TokenType.Keyword || (string)token.Value != "xref") if (token.Type != PdfLexer.TokenType.Keyword || (string)token.Value != "xref")
@ -885,23 +902,17 @@ internal class PdfMetadataExtractor : IPdfMetadataExtractor
var numObj = (long)token.Value; var numObj = (long)token.Value;
if (_objectOffsets.Length < startObj + numObj)
{
Array.Resize(ref _objectOffsets, (int)(startObj + numObj));
}
_lexer.ExpectNewline(); _lexer.ExpectNewline();
var generation = 0;
for (var obj = startObj; obj < startObj + numObj; ++obj) for (var obj = startObj; obj < startObj + numObj; ++obj)
{ {
var inUse = _lexer.NextXRefEntry(ref _objectOffsets[obj], ref generation); var inUse = _lexer.NextXRefEntry(out var offset, out var generation);
if (!inUse) if (inUse && offset > 0)
{ {
_objectOffsets[obj] = 0; _objectOffsets[obj] = offset ;
} }
// Free objects (inUse == false) are not stored in the dictionary
} }
} }
else if (token.Type == PdfLexer.TokenType.Keyword && (string)token.Value == "trailer") else if (token.Type == PdfLexer.TokenType.Keyword && (string)token.Value == "trailer")
@ -1105,11 +1116,6 @@ internal class PdfMetadataExtractor : IPdfMetadataExtractor
{ {
var section = sections.Dequeue(); var section = sections.Dequeue();
if (_objectOffsets.Length < size)
{
Array.Resize(ref _objectOffsets, (int)size);
}
for (var i = section.First; i < section.First + section.Count; ++i) for (var i = section.First; i < section.First + section.Count; ++i)
{ {
long type = 0; long type = 0;
@ -1136,9 +1142,9 @@ internal class PdfMetadataExtractor : IPdfMetadataExtractor
generation = (generation << 8) | (ushort)stream.ReadByte(); generation = (generation << 8) | (ushort)stream.ReadByte();
} }
if (type == 1 && _objectOffsets[i] == 0) if (type == 1)
{ {
_objectOffsets[i] = offset; _objectOffsets.TryAdd(i, offset);
} }
} }
} }
@ -1253,7 +1259,7 @@ internal class PdfMetadataExtractor : IPdfMetadataExtractor
{ {
var meta = _metadataRef.Pop(); var meta = _metadataRef.Pop();
//_logger.LogTrace("DocumentCatalog for {Path}: {Root}, Info: {Info}", filename, meta.root, meta.info); _logger.LogTrace("DocumentCatalog for {Path}: {Root}, Info: {Info}", filename, meta.Root, meta.Info);
ReadMetadataFromInfo(meta.Info); ReadMetadataFromInfo(meta.Info);
ReadMetadataFromXml(MetadataObjInObjectCatalog(meta.Root)); ReadMetadataFromXml(MetadataObjInObjectCatalog(meta.Root));
@ -1265,7 +1271,7 @@ internal class PdfMetadataExtractor : IPdfMetadataExtractor
// Document information dictionary (PDF Spec 14.3.3) // Document information dictionary (PDF Spec 14.3.3)
// We treat this as less authoritative than the Metadata stream. // We treat this as less authoritative than the Metadata stream.
if (infoObj < 1 || infoObj >= _objectOffsets.Length || _objectOffsets[infoObj] == 0) if (!HasObject(infoObj))
{ {
return; return;
} }
@ -1338,7 +1344,7 @@ internal class PdfMetadataExtractor : IPdfMetadataExtractor
{ {
// Look for /Metadata entry in document catalog (PDF Spec 7.7.2) // Look for /Metadata entry in document catalog (PDF Spec 7.7.2)
if (rootObj < 1 || rootObj >= _objectOffsets.Length || _objectOffsets[rootObj] == 0) if (!HasObject(rootObj))
{ {
return -1; return -1;
} }
@ -1416,7 +1422,7 @@ internal class PdfMetadataExtractor : IPdfMetadataExtractor
private void ReadMetadataFromXml(long meta) private void ReadMetadataFromXml(long meta)
{ {
if (meta < 1 || meta >= _objectOffsets.Length || _objectOffsets[meta] == 0) return; if (!HasObject(meta)) return;
_stream.Seek(_objectOffsets[meta], SeekOrigin.Begin); _stream.Seek(_objectOffsets[meta], SeekOrigin.Begin);
_lexer.ResetBuffer(); _lexer.ResetBuffer();
@ -1634,4 +1640,28 @@ internal class PdfMetadataExtractor : IPdfMetadataExtractor
SkipValue(); SkipValue();
} }
} }
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed || !disposing) return;
_stream.Dispose();
_disposed = true;
}
private bool HasObject(long objNum)
{
return _objectOffsets.ContainsKey(objNum) && _objectOffsets[objNum] > 0;
}
private long GetObjectOffset(long objNum)
{
return _objectOffsets.TryGetValue(objNum, out var offset) ? offset : 0;
}
} }

View File

@ -679,11 +679,9 @@ public class BookService : IBookService
{ {
return _pdfComicInfoExtractor.GetComicInfo(filePath); return _pdfComicInfoExtractor.GetComicInfo(filePath);
} }
else
{
return GetEpubComicInfo(filePath); return GetEpubComicInfo(filePath);
} }
}
private static void ExtractSortTitle(EpubMetadataMeta metadataItem, EpubBookRef epubBook, ComicInfo info) private static void ExtractSortTitle(EpubMetadataMeta metadataItem, EpubBookRef epubBook, ComicInfo info)
{ {

View File

@ -30,8 +30,9 @@ public interface IImageService
/// <param name="fileName"></param> /// <param name="fileName"></param>
/// <param name="encodeFormat">Convert and save as encoding format</param> /// <param name="encodeFormat">Convert and save as encoding format</param>
/// <param name="thumbnailWidth">Width of thumbnail</param> /// <param name="thumbnailWidth">Width of thumbnail</param>
/// <returns>File name with extension of the file. This will always write to <see cref="DirectoryService.CoverImageDirectory"/></returns> /// <param name="targetDirectory">If null, will write to <see cref="DirectoryService.CoverImageDirectory"/></param>
string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = 320); /// <returns>File name with extension of the file. </returns>
string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = 320, string? targetDirectory = null);
/// <summary> /// <summary>
/// Writes out a thumbnail by stream input /// Writes out a thumbnail by stream input
/// </summary> /// </summary>
@ -576,14 +577,16 @@ public class ImageService : IImageService
/// <inheritdoc /> /// <inheritdoc />
public string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = ThumbnailWidth) public string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = ThumbnailWidth, string? targetDirectory = null)
{ {
// TODO: This code has no concept of cropping nor Thumbnail Size // TODO: This code has no concept of cropping nor Thumbnail Size
try try
{ {
targetDirectory ??= _directoryService.CoverImageDirectory;
using var thumbnail = Image.ThumbnailBuffer(Convert.FromBase64String(encodedImage), thumbnailWidth); using var thumbnail = Image.ThumbnailBuffer(Convert.FromBase64String(encodedImage), thumbnailWidth);
fileName += encodeFormat.GetExtension(); fileName += encodeFormat.GetExtension();
thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, fileName)); thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(targetDirectory, fileName));
return fileName; return fileName;
} }
catch (Exception e) catch (Exception e)

View File

@ -478,6 +478,8 @@ public class CoverDbService : ICoverDbService
var format = ImageService.GetPersonFormat(person.Id); var format = ImageService.GetPersonFormat(person.Id);
var finalFileName = format + ".webp"; var finalFileName = format + ".webp";
var tempFileName = format + "_new"; var tempFileName = format + "_new";
// This is writing the image to CoverDirectory
var tempFilePath = await CreateThumbnail(url, tempFileName, fromBase64, tempDir); var tempFilePath = await CreateThumbnail(url, tempFileName, fromBase64, tempDir);
if (!string.IsNullOrEmpty(tempFilePath)) if (!string.IsNullOrEmpty(tempFilePath))
@ -719,7 +721,7 @@ public class CoverDbService : ICoverDbService
/// <param name="url"></param> /// <param name="url"></param>
/// <param name="filenameWithoutExtension">Filename without extension</param> /// <param name="filenameWithoutExtension">Filename without extension</param>
/// <param name="fromBase64"></param> /// <param name="fromBase64"></param>
/// <param name="targetDirectory">Not useable with fromBase64. Allows a different directory to be written to</param> /// <param name="targetDirectory">Allows a different directory to be written to</param>
/// <returns></returns> /// <returns></returns>
private async Task<string> CreateThumbnail(string url, string filenameWithoutExtension, bool fromBase64 = true, string? targetDirectory = null) private async Task<string> CreateThumbnail(string url, string filenameWithoutExtension, bool fromBase64 = true, string? targetDirectory = null)
{ {
@ -732,7 +734,7 @@ public class CoverDbService : ICoverDbService
if (fromBase64) if (fromBase64)
{ {
return _imageService.CreateThumbnailFromBase64(url, return _imageService.CreateThumbnailFromBase64(url,
filenameWithoutExtension, encodeFormat, coverImageSize.GetDimensions().Width); filenameWithoutExtension, encodeFormat, coverImageSize.GetDimensions().Width, targetDirectory);
} }
return await DownloadImageFromUrl(filenameWithoutExtension, encodeFormat, url, targetDirectory); return await DownloadImageFromUrl(filenameWithoutExtension, encodeFormat, url, targetDirectory);

View File

@ -118,83 +118,6 @@ public static partial class Parser
MatchOptions, RegexTimeout); MatchOptions, RegexTimeout);
private static readonly Regex[] MangaVolumeRegex =
[
// Thai Volume: เล่ม n -> Volume n
new Regex(
@"(เล่ม|เล่มที่)(\s)?(\.?)(\s|_)?(?<Volume>\d+(\-\d+)?(\.\d+)?)",
MatchOptions, RegexTimeout),
// Dance in the Vampire Bund v16-17
new Regex(
@"(?<Series>.*)(\b|_)v(?<Volume>\d+-?\d+)( |_)",
MatchOptions, RegexTimeout),
// Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.31 Omake
new Regex(
@"^(?<Series>.+?)(\s*Chapter\s*\d+)?(\s|_|\-\s)+(Vol(ume)?\.?(\s|_)?)(?<Volume>\d+(\.\d+)?)(.+?|$)",
MatchOptions, RegexTimeout),
// Historys Strongest Disciple Kenichi_v11_c90-98.zip or Dance in the Vampire Bund v16-17
new Regex(
@"(?<Series>.*)(\b|_)(?!\[)v(?<Volume>" + NumberRange + @")(?!\])",
MatchOptions, RegexTimeout),
// Kodomo no Jikan vol. 10, [dmntsf.net] One Piece - Digital Colored Comics Vol. 20.5-21.5 Ch. 177
new Regex(
@"(?<Series>.*)(\b|_)(vol\.? ?)(?<Volume>\d+(\.\d)?(-\d+)?(\.\d)?)",
MatchOptions, RegexTimeout),
// Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)
new Regex(
@"(vol\.? ?)(?<Volume>\d+(\.\d)?)",
MatchOptions, RegexTimeout),
// Tonikaku Cawaii [Volume 11].cbz
new Regex(
@"(volume )(?<Volume>\d+(\.\d)?)",
MatchOptions, RegexTimeout),
// Tower Of God S01 014 (CBT) (digital).cbz
new Regex(
@"(?<Series>.*)(\b|_|)(S(?<Volume>\d+))",
MatchOptions, RegexTimeout),
// vol_001-1.cbz for MangaPy default naming convention
new Regex(
@"(vol_)(?<Volume>\d+(\.\d)?)",
MatchOptions, RegexTimeout),
// Chinese Volume: 第n卷 -> Volume n, 第n册 -> Volume n, 幽游白书完全版 第03卷 天下 or 阿衰online 第1册
new Regex(
@"第(?<Volume>\d+)(卷|册)",
MatchOptions, RegexTimeout),
// Chinese Volume: 卷n -> Volume n, 册n -> Volume n
new Regex(
@"(卷|册)(?<Volume>\d+)",
MatchOptions, RegexTimeout),
// Korean Volume: 제n화|회|장 -> Volume n, n화|권|장 -> Volume n, 63권#200.zip -> Volume 63 (no chapter, #200 is just files inside)
new Regex(
@"제?(?<Volume>\d+(\.\d+)?)(권|화|장)",
MatchOptions, RegexTimeout),
// Korean Season: 시즌n -> Season n,
new Regex(
@"시즌(?<Volume>\d+\-?\d+)",
MatchOptions, RegexTimeout),
// Korean Season: 시즌n -> Season n, n시즌 -> season n
new Regex(
@"(?<Volume>\d+(\-|~)?\d+?)시즌",
MatchOptions, RegexTimeout),
// Korean Season: 시즌n -> Season n, n시즌 -> season n
new Regex(
@"시즌(?<Volume>\d+(\-|~)?\d+?)",
MatchOptions, RegexTimeout),
// Japanese Volume: n巻 -> Volume n
new Regex(
@"(?<Volume>\d+(?:(\-)\d+)?)巻",
MatchOptions, RegexTimeout),
// Russian Volume: Том n -> Volume n, Тома n -> Volume
new Regex(
@"Том(а?)(\.?)(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)",
MatchOptions, RegexTimeout),
// Russian Volume: n Том -> Volume n
new Regex(
@"(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)(\s|_)Том(а?)",
MatchOptions, RegexTimeout)
];
private static readonly Regex[] MangaSeriesRegex = private static readonly Regex[] MangaSeriesRegex =
[ [
// Thai Volume: เล่ม n -> Volume n // Thai Volume: เล่ม n -> Volume n
@ -239,7 +162,7 @@ public static partial class Parser
RegexTimeout), RegexTimeout),
// Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA], Black Bullet - v4 c17 [batoto] // Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA], Black Bullet - v4 c17 [batoto]
new Regex( new Regex(
@"(?<Series>.+?)( - )(?:v|vo|c|chapters)\d", @"(?<Series>.+?)( - )(?:v|vo|c|chapters|tome|t|ch)\d",
MatchOptions, RegexTimeout), MatchOptions, RegexTimeout),
// Kedouin Makoto - Corpse Party Musume, Chapter 19 [Dametrans].zip // Kedouin Makoto - Corpse Party Musume, Chapter 19 [Dametrans].zip
new Regex( new Regex(
@ -251,14 +174,18 @@ public static partial class Parser
MatchOptions, RegexTimeout), MatchOptions, RegexTimeout),
// [dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz // [dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz
new Regex( new Regex(
@"(?<Series>.+?):? (\b|_|-)(vol)\.?(\s|-|_)?\d+", @"(?<Series>.+?):? (\b|_|-)(vol|tome)\.?(\s|-|_)?\d+",
MatchOptions, RegexTimeout), MatchOptions, RegexTimeout),
// [xPearse] Kyochuu Rettou Chapter 001 Volume 1 [English] [Manga] [Volume Scans] // [xPearse] Kyochuu Rettou Chapter 001 Volume 1 [English] [Manga] [Volume Scans]
new Regex( new Regex(
@"(?<Series>.+?):?(\s|\b|_|-)Chapter(\s|\b|_|-)\d+(\s|\b|_|-)(vol)(ume)", @"(?<Series>.+?):?(\s|\b|_|-)Chapter(\s|\b|_|-)\d+(\s|\b|_|-)(vol)(ume)",
MatchOptions, MatchOptions,
RegexTimeout), RegexTimeout),
// Kyochuu Rettou T3, Kyochuu Rettou - Tome 3
new Regex(
@"(?<Series>.+?):? (\b|_|-)(t\d+|tome(\b|_)\d+)",
MatchOptions,
RegexTimeout),
// [xPearse] Kyochuu Rettou Volume 1 [English] [Manga] [Volume Scans] // [xPearse] Kyochuu Rettou Volume 1 [English] [Manga] [Volume Scans]
new Regex( new Regex(
@"(?<Series>.+?):? (\b|_|-)(vol)(ume)", @"(?<Series>.+?):? (\b|_|-)(vol)(ume)",
@ -270,7 +197,7 @@ public static partial class Parser
MatchOptions, RegexTimeout), MatchOptions, RegexTimeout),
//Tonikaku Cawaii [Volume 11], Darling in the FranXX - Volume 01.cbz //Tonikaku Cawaii [Volume 11], Darling in the FranXX - Volume 01.cbz
new Regex( new Regex(
@"(?<Series>.*)(?: _|-|\[|\()\s?vol(ume)?", @"(?<Series>.*)(?: _|-|\[|\()\s?(vol(ume)?|tome|t\d+)",
MatchOptions, RegexTimeout), MatchOptions, RegexTimeout),
// Momo The Blood Taker - Chapter 027 Violent Emotion.cbz, Grand Blue Dreaming - SP02 Extra (2019) (Digital) (danke-Empire).cbz // Momo The Blood Taker - Chapter 027 Violent Emotion.cbz, Grand Blue Dreaming - SP02 Extra (2019) (Digital) (danke-Empire).cbz
new Regex( new Regex(
@ -465,6 +392,83 @@ public static partial class Parser
MatchOptions, RegexTimeout) MatchOptions, RegexTimeout)
]; ];
private static readonly Regex[] MangaVolumeRegex =
[
// Thai Volume: เล่ม n -> Volume n
new Regex(
@"(เล่ม|เล่มที่)(\s)?(\.?)(\s|_)?(?<Volume>\d+(\-\d+)?(\.\d+)?)",
MatchOptions, RegexTimeout),
// Dance in the Vampire Bund v16-17, Dance in the Vampire Bund Tome 1
new Regex(
@"(?<Series>.*)(\b|_)(v|tome(\s|_)?|t)(?<Volume>\d+-?\d+)(\s|_)",
MatchOptions, RegexTimeout),
// Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.31 Omake
new Regex(
@"^(?<Series>.+?)(\s*Chapter\s*\d+)?(\s|_|\-\s)+((Vol(ume)?|tome)\.?(\s|_)?)(?<Volume>\d+(\.\d+)?)(.+?|$)",
MatchOptions, RegexTimeout),
// Historys Strongest Disciple Kenichi_v11_c90-98.zip or Dance in the Vampire Bund v16-17
new Regex(
@"(?<Series>.*)(\b|_)(?!\[)v(?<Volume>" + NumberRange + @")(?!\])",
MatchOptions, RegexTimeout),
// Kodomo no Jikan vol. 10, [dmntsf.net] One Piece - Digital Colored Comics Vol. 20.5-21.5 Ch. 177
new Regex(
@"(?<Series>.*)(\b|_)(vol\.? ?)(?<Volume>\d+(\.\d)?(-\d+)?(\.\d)?)",
MatchOptions, RegexTimeout),
// Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)
new Regex(
@"(vol\.? ?)(?<Volume>\d+(\.\d)?)",
MatchOptions, RegexTimeout),
// Tonikaku Cawaii [Volume 11].cbz
new Regex(
@"((volume|tome)\s)(?<Volume>\d+(\.\d)?)",
MatchOptions, RegexTimeout),
// Tower Of God S01 014 (CBT) (digital).cbz, Tower Of God T01 014 (CBT) (digital).cbz,
new Regex(
@"(?<Series>.*)(\b|_)((S|T)(?<Volume>\d+))",
MatchOptions, RegexTimeout),
// vol_001-1.cbz for MangaPy default naming convention
new Regex(
@"(vol_)(?<Volume>\d+(\.\d)?)",
MatchOptions, RegexTimeout),
// Chinese Volume: 第n卷 -> Volume n, 第n册 -> Volume n, 幽游白书完全版 第03卷 天下 or 阿衰online 第1册
new Regex(
@"第(?<Volume>\d+)(卷|册)",
MatchOptions, RegexTimeout),
// Chinese Volume: 卷n -> Volume n, 册n -> Volume n
new Regex(
@"(卷|册)(?<Volume>\d+)",
MatchOptions, RegexTimeout),
// Korean Volume: 제n화|회|장 -> Volume n, n화|권|장 -> Volume n, 63권#200.zip -> Volume 63 (no chapter, #200 is just files inside)
new Regex(
@"제?(?<Volume>\d+(\.\d+)?)(권|화|장)",
MatchOptions, RegexTimeout),
// Korean Season: 시즌n -> Season n,
new Regex(
@"시즌(?<Volume>\d+\-?\d+)",
MatchOptions, RegexTimeout),
// Korean Season: 시즌n -> Season n, n시즌 -> season n
new Regex(
@"(?<Volume>\d+(\-|~)?\d+?)시즌",
MatchOptions, RegexTimeout),
// Korean Season: 시즌n -> Season n, n시즌 -> season n
new Regex(
@"시즌(?<Volume>\d+(\-|~)?\d+?)",
MatchOptions, RegexTimeout),
// Japanese Volume: n巻 -> Volume n
new Regex(
@"(?<Volume>\d+(?:(\-)\d+)?)巻",
MatchOptions, RegexTimeout),
// Russian Volume: Том n -> Volume n, Тома n -> Volume
new Regex(
@"Том(а?)(\.?)(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)",
MatchOptions, RegexTimeout),
// Russian Volume: n Том -> Volume n
new Regex(
@"(\s|_)?(?<Volume>\d+(?:(\-)\d+)?)(\s|_)Том(а?)",
MatchOptions, RegexTimeout)
];
private static readonly Regex[] ComicVolumeRegex = private static readonly Regex[] ComicVolumeRegex =
[ [
// Thai Volume: เล่ม n -> Volume n // Thai Volume: เล่ม n -> Volume n

View File

@ -12,9 +12,9 @@
<PackageReference Include="Cronos" Version="0.11.0" /> <PackageReference Include="Cronos" Version="0.11.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.3" /> <PackageReference Include="DotNet.Glob" Version="3.1.3" />
<PackageReference Include="Flurl.Http" Version="4.0.2" /> <PackageReference Include="Flurl.Http" Version="4.0.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.6" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.6" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.7" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.11.0.117924"> <PackageReference Include="SonarAnalyzer.CSharp" Version="10.15.0.120848">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View File

@ -1,11 +1,11 @@
import { HttpClient } from '@angular/common/http'; import {HttpClient} from '@angular/common/http';
import {DestroyRef, Injectable} from '@angular/core'; import {DestroyRef, Injectable} from '@angular/core';
import { of } from 'rxjs'; import {of} from 'rxjs';
import {filter, map, tap} from 'rxjs/operators'; import {filter, map, tap} from 'rxjs/operators';
import { environment } from 'src/environments/environment'; import {environment} from 'src/environments/environment';
import { JumpKey } from '../_models/jumpbar/jump-key'; import {JumpKey} from '../_models/jumpbar/jump-key';
import { Library, LibraryType } from '../_models/library/library'; import {Library, LibraryType} from '../_models/library/library';
import { DirectoryDto } from '../_models/system/directory-dto'; import {DirectoryDto} from '../_models/system/directory-dto';
import {EVENTS, MessageHubService} from "./message-hub.service"; import {EVENTS, MessageHubService} from "./message-hub.service";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@ -73,6 +73,10 @@ export class LibraryService {
return this.httpClient.get<DirectoryDto[]>(this.baseUrl + 'library/list' + query); return this.httpClient.get<DirectoryDto[]>(this.baseUrl + 'library/list' + query);
} }
hasFilesAtRoot(roots: Array<string>) {
return this.httpClient.post<{[key: string]: boolean}>(this.baseUrl + 'library/has-files-at-root', {roots});
}
getJumpBar(libraryId: number) { getJumpBar(libraryId: number) {
return this.httpClient.get<JumpKey[]>(this.baseUrl + 'library/jump-bar?libraryId=' + libraryId); return this.httpClient.get<JumpKey[]>(this.baseUrl + 'library/jump-bar?libraryId=' + libraryId);
} }

View File

@ -78,6 +78,12 @@ export class PersonService {
); );
} }
isValidAsin(asin: string) {
return this.httpClient.get<boolean>(this.baseUrl + `person/valid-asin?asin=${asin}`, TextResonse).pipe(
map(valid => valid + '' === 'true')
);
}
mergePerson(destId: number, srcId: number) { mergePerson(destId: number, srcId: number) {
return this.httpClient.post<Person>(this.baseUrl + 'person/merge', {destId, srcId}); return this.httpClient.post<Person>(this.baseUrl + 'person/merge', {destId, srcId});
} }

View File

@ -10,13 +10,13 @@ import {
Output Output
} from '@angular/core'; } from '@angular/core';
import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms'; import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
import {NgxFileDropEntry, FileSystemFileEntry, NgxFileDropModule} from 'ngx-file-drop'; import {FileSystemFileEntry, NgxFileDropEntry, NgxFileDropModule} from 'ngx-file-drop';
import { fromEvent } from 'rxjs'; import {fromEvent} from 'rxjs';
import { takeWhile } from 'rxjs/operators'; import {takeWhile} from 'rxjs/operators';
import { ToastrService } from 'ngx-toastr'; import {ToastrService} from 'ngx-toastr';
import { ImageService } from 'src/app/_services/image.service'; import {ImageService} from 'src/app/_services/image.service';
import { KEY_CODES } from 'src/app/shared/_services/utility.service'; import {KEY_CODES} from 'src/app/shared/_services/utility.service';
import { UploadService } from 'src/app/_services/upload.service'; import {UploadService} from 'src/app/_services/upload.service';
import {DOCUMENT, NgClass} from '@angular/common'; import {DOCUMENT, NgClass} from '@angular/common';
import {ImageComponent} from "../../shared/image/image.component"; import {ImageComponent} from "../../shared/image/image.component";
import {translate, TranslocoModule} from "@jsverse/transloco"; import {translate, TranslocoModule} from "@jsverse/transloco";
@ -233,7 +233,8 @@ export class CoverImageChooserComponent implements OnInit {
this.imageSelected.emit(this.selectedIndex); // Auto select newly uploaded image this.imageSelected.emit(this.selectedIndex); // Auto select newly uploaded image
this.selectedBase64Url.emit(e.target.result); this.selectedBase64Url.emit(e.target.result);
setTimeout(() => { setTimeout(() => {
(this.document.querySelector('div.image-card[aria-label="Image ' + this.selectedIndex + '"]') as HTMLElement).focus(); // Add 1 since we are adding a new image
(this.document.querySelector('div.clickable[aria-label="Image ' + (this.selectedIndex + 1) + '"]') as HTMLElement).focus();
}) })
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }

View File

@ -73,7 +73,13 @@
<ng-template #view> <ng-template #view>
<input id="asin" class="form-control" formControlName="asin" type="text" <input id="asin" class="form-control" formControlName="asin" type="text"
[class.is-invalid]="formControl.invalid && !formControl.untouched"> [class.is-invalid]="formControl.invalid && !formControl.untouched">
@if (formControl.errors; as errors) {
<div class="invalid-feedback">
@if (errors.invalidAsin) {
<div>{{t('invalid-asin')}}</div>
}
</div>
}
</ng-template> </ng-template>
</app-setting-item> </app-setting-item>

View File

@ -73,7 +73,7 @@ export class EditPersonModalComponent implements OnInit {
editForm: FormGroup = new FormGroup({ editForm: FormGroup = new FormGroup({
name: new FormControl('', [Validators.required]), name: new FormControl('', [Validators.required]),
description: new FormControl('', []), description: new FormControl('', []),
asin: new FormControl('', []), asin: new FormControl('', [], [this.asinValidator()]),
aniListId: new FormControl('', []), aniListId: new FormControl('', []),
malId: new FormControl('', []), malId: new FormControl('', []),
hardcoverId: new FormControl('', []), hardcoverId: new FormControl('', []),
@ -194,4 +194,21 @@ export class EditPersonModalComponent implements OnInit {
} }
} }
asinValidator(): AsyncValidatorFn {
return (control: AbstractControl) => {
const asin = control.value;
if (!asin || asin.trim().length === 0) {
return of(null);
}
return this.personService.isValidAsin(asin).pipe(map(valid => {
if (valid) {
return null;
}
return { 'invalidAsin': {'asin': asin} } as ValidationErrors;
}));
}
}
} }

View File

@ -69,6 +69,11 @@
<li [ngbNavItem]="TabID.Folder" [disabled]="isAddLibrary && setupStep < 1"> <li [ngbNavItem]="TabID.Folder" [disabled]="isAddLibrary && setupStep < 1">
<a ngbNavLink>{{t(TabID.Folder)}}</a> <a ngbNavLink>{{t(TabID.Folder)}}</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
@if (filesAtRoot()) {
<p class="alert alert-warning">{{t('files-at-root-warning')}}</p>
}
<p>{{t('folder-description')}}</p> <p>{{t('folder-description')}}</p>
<ul class="list-group list-group-flush" style="width: 100%"> <ul class="list-group list-group-flush" style="width: 100%">
@for(folder of selectedFolders; track folder) { @for(folder of selectedFolders; track folder) {
@ -77,7 +82,6 @@
<button class="btn float-end btn-sm" (click)="removeFolder(folder)"><i class="fa fa-times-circle" aria-hidden="true"></i></button> <button class="btn float-end btn-sm" (click)="removeFolder(folder)"><i class="fa fa-times-circle" aria-hidden="true"></i></button>
</li> </li>
} }
</ul> </ul>
<div class="row mt-2"> <div class="row mt-2">
<button class="btn btn-secondary float-end btn-sm" (click)="openDirectoryPicker()"> <button class="btn btn-secondary float-end btn-sm" (click)="openDirectoryPicker()">

View File

@ -1,4 +1,13 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, Input, OnInit} from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef,
inject,
Input,
model,
OnInit
} from '@angular/core';
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import { import {
NgbActiveModal, NgbActiveModal,
@ -13,7 +22,6 @@ import {
} from '@ng-bootstrap/ng-bootstrap'; } from '@ng-bootstrap/ng-bootstrap';
import {ToastrService} from 'ngx-toastr'; import {ToastrService} from 'ngx-toastr';
import {debounceTime, distinctUntilChanged, switchMap, tap} from 'rxjs'; import {debounceTime, distinctUntilChanged, switchMap, tap} from 'rxjs';
import {SettingsService} from 'src/app/admin/settings.service';
import { import {
DirectoryPickerComponent, DirectoryPickerComponent,
DirectoryPickerResult DirectoryPickerResult
@ -78,7 +86,6 @@ export class LibrarySettingsModalComponent implements OnInit {
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
private readonly uploadService = inject(UploadService); private readonly uploadService = inject(UploadService);
private readonly modalService = inject(NgbModal); private readonly modalService = inject(NgbModal);
private readonly settingService = inject(SettingsService);
private readonly confirmService = inject(ConfirmService); private readonly confirmService = inject(ConfirmService);
private readonly libraryService = inject(LibraryService); private readonly libraryService = inject(LibraryService);
private readonly toastr = inject(ToastrService); private readonly toastr = inject(ToastrService);
@ -128,6 +135,7 @@ export class LibrarySettingsModalComponent implements OnInit {
setupStep = StepID.General; setupStep = StepID.General;
fileTypeGroups = allFileTypeGroup; fileTypeGroups = allFileTypeGroup;
excludePatterns: Array<string> = ['']; excludePatterns: Array<string> = [''];
filesAtRoot = model<boolean>(false);
tasks: ActionItem<Library>[] = this.getTasks(); tasks: ActionItem<Library>[] = this.getTasks();
@ -145,6 +153,8 @@ export class LibrarySettingsModalComponent implements OnInit {
if (this.library === undefined) { if (this.library === undefined) {
this.isAddLibrary = true; this.isAddLibrary = true;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} else {
this.checkForFilesAtRoot();
} }
if (this.library?.coverImage != null && this.library?.coverImage !== '') { if (this.library?.coverImage != null && this.library?.coverImage !== '') {
@ -310,7 +320,14 @@ export class LibrarySettingsModalComponent implements OnInit {
} }
isDisabled() { isDisabled() {
return !(this.libraryForm.valid && this.selectedFolders.length > 0); const selectedFileTypes = [];
for(let fileTypeGroup of allFileTypeGroup) {
if (this.libraryForm.value[fileTypeGroup]) {
selectedFileTypes.push(fileTypeGroup);
}
}
return !(this.libraryForm.valid && this.selectedFolders.length > 0 && selectedFileTypes.length > 0);
} }
reset() { reset() {
@ -340,6 +357,7 @@ export class LibrarySettingsModalComponent implements OnInit {
} }
model.excludePatterns = this.excludePatterns; model.excludePatterns = this.excludePatterns;
if (this.libraryForm.errors) { if (this.libraryForm.errors) {
return; return;
} }
@ -402,6 +420,7 @@ export class LibrarySettingsModalComponent implements OnInit {
if (!this.selectedFolders.includes(closeResult.folderPath)) { if (!this.selectedFolders.includes(closeResult.folderPath)) {
this.selectedFolders.push(closeResult.folderPath); this.selectedFolders.push(closeResult.folderPath);
this.madeChanges = true; this.madeChanges = true;
this.checkForFilesAtRoot();
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }
} }
@ -411,6 +430,7 @@ export class LibrarySettingsModalComponent implements OnInit {
removeFolder(folder: string) { removeFolder(folder: string) {
this.selectedFolders = this.selectedFolders.filter(item => item !== folder); this.selectedFolders = this.selectedFolders.filter(item => item !== folder);
this.madeChanges = true; this.madeChanges = true;
this.checkForFilesAtRoot();
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }
@ -453,4 +473,18 @@ export class LibrarySettingsModalComponent implements OnInit {
break; break;
} }
} }
checkForFilesAtRoot() {
this.libraryService.hasFilesAtRoot(this.selectedFolders).subscribe(results => {
let containsMultipleFiles = false;
Object.keys(results).forEach(key => {
if (results[key]) {
containsMultipleFiles = true;
return;
}
});
this.filesAtRoot.set(containsMultipleFiles);
})
}
} }

View File

@ -1144,7 +1144,8 @@
"file-type-group-tooltip": "What types of files should Kavita scan for. For example, Archive will include all cb*, zip, rar, etc files.", "file-type-group-tooltip": "What types of files should Kavita scan for. For example, Archive will include all cb*, zip, rar, etc files.",
"exclude-patterns-label": "Exclude Patterns", "exclude-patterns-label": "Exclude Patterns",
"exclude-patterns-tooltip": "Configure a set of patterns (Glob syntax) that Kavita will match when scanning directories and exclude from Scanner results.", "exclude-patterns-tooltip": "Configure a set of patterns (Glob syntax) that Kavita will match when scanning directories and exclude from Scanner results.",
"help": "{{common.help}}" "help": "{{common.help}}",
"files-at-root-warning": "One or more folders contains files at the root. Kavita does not support this."
}, },
"file-type-group-pipe": { "file-type-group-pipe": {
@ -2272,6 +2273,7 @@
"hardcover-tooltip": "https://hardcover.app/authors/{HardcoverId}", "hardcover-tooltip": "https://hardcover.app/authors/{HardcoverId}",
"asin-label": "ASIN", "asin-label": "ASIN",
"asin-tooltip": "https://www.amazon.com/stores/J.K.-Rowling/author/{ASIN}", "asin-tooltip": "https://www.amazon.com/stores/J.K.-Rowling/author/{ASIN}",
"invalid-asin": "ASIN must be a valid ISBN-10 or ISBN-13 format",
"description-label": "Description", "description-label": "Description",
"required-field": "{{validations.required-field}}", "required-field": "{{validations.required-field}}",
"cover-image-description": "{{edit-series-modal.cover-image-description}}", "cover-image-description": "{{edit-series-modal.cover-image-description}}",