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>
<PackageReference Include="BenchmarkDotNet" Version="0.15.1" />
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.15.1" />
<PackageReference Include="BenchmarkDotNet" Version="0.15.2" />
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.15.2" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
</ItemGroup>

View File

@ -6,13 +6,13 @@
</PropertyGroup>
<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="NSubstitute" Version="5.3.0" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="22.0.14" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.0.14" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="22.0.15" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.0.15" />
<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>
<PrivateAssets>all</PrivateAssets>
</PackageReference>

View File

@ -46,8 +46,8 @@ public class DefaultParserTests
[Theory]
[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/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/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/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", Parser.LooseLeafVolume, Parser.DefaultChapter})]
public void ParseFromFallbackFolders_ShouldParseSeriesVolumeAndChapter(string inputFile, string[] expectedParseInfo)
{
const string rootDirectory = "/manga/";
@ -119,7 +119,7 @@ public class DefaultParserTests
expected.Add(filepath, new ParserInfo
{
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
});
@ -144,7 +144,7 @@ public class DefaultParserTests
expected.Add(filepath, new ParserInfo
{
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
});
@ -152,7 +152,7 @@ public class DefaultParserTests
expected.Add(filepath, new ParserInfo
{
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
});
@ -160,14 +160,14 @@ public class DefaultParserTests
expected.Add(filepath, new ParserInfo
{
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
});
filepath = @"E:/Manga/APOSIMZ/APOSIMZ 040 (2020) (Digital) (danke-Empire).cbz";
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,
FullFilePath = filepath
});
@ -175,7 +175,7 @@ public class DefaultParserTests
filepath = @"E:/Manga/Corpse Party Musume/Kedouin Makoto - Corpse Party Musume, Chapter 09.cbz";
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,
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";
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,
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";
expected.Add(filepath, new ParserInfo
{
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,
Series = "Summer Time Rendering", Volumes = Parser.SpecialVolume, Edition = "",
Chapters = Parser.DefaultChapter, Filename = "Record 014 (between chapter 083 and ch084) SP11.cbr", Format = MangaFormat.Archive,
FullFilePath = filepath, IsSpecial = true
});
filepath = @"E:/Manga/Seraph of the End/Seraph of the End - Vampire Reign 093 (2020) (Digital) (LuCaZ).cbz";
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,
FullFilePath = filepath, IsSpecial = false
});
@ -227,7 +227,7 @@ public class DefaultParserTests
filepath = @"E:/Manga/The Beginning After the End/Chapter 001.cbz";
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,
FullFilePath = filepath, IsSpecial = false
});
@ -236,7 +236,7 @@ public class DefaultParserTests
expected.Add(filepath, new ParserInfo
{
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
});
@ -244,7 +244,7 @@ public class DefaultParserTests
expected.Add(filepath, new ParserInfo
{
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
});
@ -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 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,
FullFilePath = filepath, IsSpecial = false
};
@ -407,7 +407,7 @@ public class DefaultParserTests
filepath = @"E:/Manga/Foo 50/Specials/Foo 50 SP01.cbz";
expected = new ParserInfo
{
Series = "Foo 50", Volumes = API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume, IsSpecial = true,
Series = "Foo 50", Volumes = Parser.SpecialVolume, IsSpecial = true,
Chapters = Parser.DefaultChapter, Filename = "Foo 50 SP01.cbz", Format = MangaFormat.Archive,
FullFilePath = filepath
};
@ -442,8 +442,8 @@ public class DefaultParserTests
var filepath = @"E:/Comics/Teen Titans/Teen Titans v1 Annual 01 (1967) SP01.cbr";
expected.Add(filepath, new ParserInfo
{
Series = "Teen Titans", Volumes = API.Services.Tasks.Scanner.Parser.Parser.SpecialVolume,
Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, Filename = "Teen Titans v1 Annual 01 (1967) SP01.cbr", Format = MangaFormat.Archive,
Series = "Teen Titans", Volumes = Parser.SpecialVolume,
Chapters = Parser.DefaultChapter, Filename = "Teen Titans v1 Annual 01 (1967) SP01.cbr", Format = MangaFormat.Archive,
FullFilePath = filepath
});
@ -451,7 +451,7 @@ public class DefaultParserTests
filepath = @"E:/Comics/Comics/Babe/Babe Vol.1 #1-4/Babe 01.cbr";
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,
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";
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,
FullFilePath = filepath, IsSpecial = false
});

View File

@ -1,4 +1,5 @@
using API.Entities.Enums;
using API.Services.Tasks.Scanner.Parser;
using Xunit;
namespace API.Tests.Parsing;
@ -17,7 +18,7 @@ public class MangaParsingTests
[InlineData("v001", "1")]
[InlineData("Vol 1", "1")]
[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("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip", "1.1")]
[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 v11 (2013) (Digital) (LostNerevarine-Empire).cbz", "11")]
[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("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("Ichiban_Ushiro_no_Daimaou_v04_ch27_[VISCANS].zip", "4")]
[InlineData("Mob Psycho 100 v02 (2019) (Digital) (Shizu).cbz", "2")]
[InlineData("Kodomo no Jikan vol. 1.cbz", "1")]
[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("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("[dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz", "20")]
[InlineData("Gantz.V26.cbz", "26")]
@ -52,7 +53,7 @@ public class MangaParsingTests
[InlineData("NEEDLESS_Vol.4_-_Simeon_6_v2_[SugoiSugoi].rar", "4")]
[InlineData("Okusama wa Shougakusei c003 (v01) [bokuwaNEET]", "1")]
[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("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")]
@ -64,7 +65,7 @@ public class MangaParsingTests
[InlineData("スライム倒して300年、知らないうちにレベルMAXになってました 1-3巻", "1-3")]
[InlineData("Dance in the Vampire Bund {Special Edition} v03.5 (2019) (Digital) (KG Manga)", "3.5")]
[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("조선왕조실톡 106화", "106")]
@ -76,9 +77,19 @@ public class MangaParsingTests
[InlineData("Accel World Volume 2", "2")]
[InlineData("Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.31 Omake", "30")]
[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)
{
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename, LibraryType.Manga));
Assert.Equal(expected, Parser.ParseVolume(filename, LibraryType.Manga));
}
[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("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 - 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)
{
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename, LibraryType.Manga));
Assert.Equal(expected, Parser.ParseSeries(filename, LibraryType.Manga));
}
[Theory]
[InlineData("Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)", "1")]
[InlineData("My Girlfriend Is Shobitch v01 - ch. 09 - pg. 008.png", "9")]
[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("BTOOOM! v01 (2013) (Digital) (Shadowcat-Empire)", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)]
[InlineData("B_Gata_H_Kei_v01[SlowManga&OverloadScans]", Parser.DefaultChapter)]
[InlineData("BTOOOM! v01 (2013) (Digital) (Shadowcat-Empire)", Parser.DefaultChapter)]
[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("[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("Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz", "18")]
[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("APOSIMZ 040 (2020) (Digital) (danke-Empire).cbz", "40")]
[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("Goblin Slayer Side Story - Year One 025.5", "25.5")]
[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("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("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_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.12 - 1 , 2 , 3P!.cbz", "12")]
[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("Deku_&_Bakugo_-_Rising_v1_c1.1.cbz", "1.1")]
[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("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("자유록 13회#2", "13")]
[InlineData("이세계에서 고아원을 열었지만, 어째서인지 아무도 독립하려 하지 않는다 38-1화 ", "38")]
[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("Манга Глава 2", "2")]
[InlineData("Манга 2 Глава", "2")]
[InlineData("Манга Том 1 2 Глава", "2")]
[InlineData("Accel World Chapter 001 Volume 002", "1")]
[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 c01-c04", "1-4")]
[InlineData("Adabana c00-02", "0-2")]
@ -299,9 +315,10 @@ public class MangaParsingTests
[InlineData("Max Level Returner ตอนที่ 5", "5")]
[InlineData("หนึ่งความคิด นิจนิรันดร์ บทที่ 112", "112")]
[InlineData("Monster #8 Ch. 001", "1")]
[InlineData("Monster Ch. 001 [MangaPlus] [Digital] [amit34521]", "1")]
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")]
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]
[InlineData("Beelzebub Special OneShot - Minna no Kochikame x Beelzebub (2016) [Mangastream].cbz", false)]
[InlineData("Beelzebub_Omake_June_2012_RHS", false)]
@ -339,7 +357,7 @@ public class MangaParsingTests
[InlineData("Hajime no Ippo - Artbook", false)]
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]
@ -348,7 +366,7 @@ public class MangaParsingTests
[InlineData("image.txt", MangaFormat.Unknown)]
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>
<PackageReference Include="CsvHelper" Version="33.1.0" />
<PackageReference Include="MailKit" Version="4.12.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6">
<PackageReference Include="MailKit" Version="4.13.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@ -66,20 +66,20 @@
<PackageReference Include="Hangfire.InMemory" Version="1.0.0" />
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
<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="Hangfire.AspNetCore" Version="1.8.20" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.7" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
<PackageReference Include="Nager.ArticleNumber" Version="1.0.7" />
<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.AspNetCore" Version="9.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="SharpCompress" Version="0.40.0" />
<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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.3" />
<PackageReference Include="System.Drawing.Common" Version="9.0.6" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.12.0" />
<PackageReference Include="System.IO.Abstractions" Version="22.0.14" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="9.0.0" />
<PackageReference Include="System.Drawing.Common" Version="9.0.7" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.13.0" />
<PackageReference Include="System.IO.Abstractions" Version="22.0.15" />
<PackageReference Include="VersOne.Epub" Version="3.3.4" />
<PackageReference Include="YamlDotNet" Version="16.3.0" />
</ItemGroup>

View File

@ -16,6 +16,7 @@ using API.Extensions;
using API.Helpers.Builders;
using API.Services;
using API.Services.Tasks.Scanner;
using API.Services.Tasks.Scanner.Parser;
using API.SignalR;
using AutoMapper;
using EasyCaching.Core;
@ -83,6 +84,7 @@ public class LibraryController : BaseApiController
.WithManageReadingLists(dto.ManageReadingLists)
.WithAllowScrobbling(dto.AllowScrobbling)
.WithAllowMetadataMatching(dto.AllowMetadataMatching)
.WithEnableMetadata(dto.EnableMetadata)
.Build();
library.LibraryFileTypes = dto.FileGroupTypes
@ -173,6 +175,26 @@ public class LibraryController : BaseApiController
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>
/// Return a specific library
/// </summary>

View File

@ -148,6 +148,18 @@ public class PersonController : BaseApiController
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>
/// Attempts to download the cover from CoversDB (Note: Not yet release in Kavita)
/// </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
{
var extractor = new PdfMetadataExtractor(_logger, filePath);
using var extractor = new PdfMetadataExtractor(_logger, 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);
_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;
}
}

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.Collections.Generic;
using System.IO.Compression;
using System.Text;
using System.Xml;
using System.IO;
using Microsoft.Extensions.Logging;
using API.Services;
using Microsoft.Extensions.Logging;
namespace API.Helpers;
#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>
/// Parse PDF file and try to extract as much metadata as possible.
/// 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 bool _secondByte = false;
private byte _prevByte = 0;
private bool _isUnicode = false;
private bool _secondByte;
private byte _prevByte;
private bool _isUnicode;
// PDFDocEncoding defined in PDF Spec D.1
@ -71,11 +99,11 @@ class PdfStringBuilder
private void AppendPdfDocByte(byte b)
{
if (b >= 0x18 && b < 0x20)
if (b is >= 0x18 and < 0x20)
{
_builder.Append(_pdfDocMappingLow[b - 0x18]);
}
else if (b >= 0x80 && b < 0xA1)
else if (b is >= 0x80 and < 0xA1)
{
_builder.Append(_pdfDocMappingHigh[b - 0x80]);
}
@ -95,28 +123,24 @@ class PdfStringBuilder
// PDF Spec 7.9.2.1: Strings are either UTF-16BE or PDFDocEncoded
if (_builder.Length == 0 && !_isUnicode)
{
// Unicode strings are prefixed by a big endian BOM \uFEFF
if (_secondByte)
switch (_secondByte)
{
if (b == 0xFF)
{
// Unicode strings are prefixed by a big endian BOM \uFEFF
case true when b == 0xFF:
_isUnicode = true;
_secondByte = false;
}
else
{
break;
case true:
AppendPdfDocByte(_prevByte);
AppendPdfDocByte(b);
}
}
else if (!_secondByte && b == 0xFE)
{
_secondByte = true;
_prevByte = b;
}
else
{
AppendPdfDocByte(b);
break;
case false when b == 0xFE:
_secondByte = true;
_prevByte = b;
break;
default:
AppendPdfDocByte(b);
break;
}
}
else if (_isUnicode)
@ -138,7 +162,7 @@ class PdfStringBuilder
}
}
override public string ToString()
public override string ToString()
{
if (_builder.Length == 0 && _secondByte)
{
@ -153,8 +177,8 @@ internal class PdfLexer(Stream stream)
{
private const int BufferSize = 1024;
private readonly byte[] _buffer = new byte[BufferSize];
private int _pos = 0;
private int _valid = 0;
private int _pos;
private int _valid;
public enum TokenType
{
@ -353,10 +377,8 @@ internal class PdfLexer(Stream stream)
{
return (long)token.Value;
}
else
{
throw new PdfMetadataExtractorException("Expected integer after startxref keyword");
}
throw new PdfMetadataExtractorException("Expected integer after startxref keyword");
}
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);
if (_valid - _pos < 20)
@ -378,14 +408,11 @@ internal class PdfLexer(Stream 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)
{
obj = Convert.ToInt64(Encoding.ASCII.GetString(_buffer, _pos, 10));
generation = Convert.ToInt32(Encoding.ASCII.GetString(_buffer, _pos + 11, 5));
inUse = _buffer[_pos + 17] == 'n';
}
var inUse = _buffer[_pos + 17] == 'n';
_pos += 20;
@ -503,7 +530,7 @@ internal class PdfLexer(Stream stream)
{
StringBuilder sb = new();
var hasDot = LastByte() == '.';
var followedBySpace = false;
bool followedBySpace;
sb.Append((char)LastByte());
@ -647,7 +674,8 @@ internal class PdfLexer(Stream stream)
case '(':
parenLevel++;
goto default;
sb.AppendByte(b);
break;
case ')':
if (--parenLevel == 0)
@ -655,7 +683,8 @@ internal class PdfLexer(Stream stream)
return new Token(TokenType.String, sb.ToString());
}
goto default;
sb.AppendByte(b);
break;
case '\\':
b = NextByte();
@ -688,7 +717,6 @@ internal class PdfLexer(Stream stream)
break;
case >= '0' and <= '7':
var b1 = b;
var b2 = NextByte();
var b3 = NextByte();
@ -697,7 +725,7 @@ internal class PdfLexer(Stream stream)
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;
}
@ -763,26 +791,15 @@ internal class PdfLexer(Stream stream)
}
}
switch (sb.ToString())
return sb.ToString() switch
{
case "true":
return new Token(TokenType.Bool, true);
case "false":
return new Token(TokenType.Bool, false);
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());
}
"true" => new Token(TokenType.Bool, true),
"false" => new Token(TokenType.Bool, false),
"stream" => new Token(TokenType.StreamStart, true),
"endstream" => new Token(TokenType.StreamEnd, true),
"endobj" => new Token(TokenType.ObjectEnd, true),
_ => new Token(TokenType.Keyword, sb.ToString())
};
}
}
@ -791,9 +808,10 @@ internal class PdfMetadataExtractor : IPdfMetadataExtractor
private readonly ILogger<BookService> _logger;
private readonly PdfLexer _lexer;
private readonly FileStream _stream;
private long[] _objectOffsets = new long[0];
private readonly Dictionary<long, long> _objectOffsets = [];
private readonly Dictionary<string, string> _metadata = [];
private readonly Stack<MetadataRef> _metadataRef = new();
private bool _disposed;
private struct MetadataRef(long root, long info)
{
@ -801,7 +819,7 @@ internal class PdfMetadataExtractor : IPdfMetadataExtractor
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 Count = count;
@ -822,7 +840,9 @@ internal class PdfMetadataExtractor : IPdfMetadataExtractor
return _metadata;
}
#pragma warning disable S1144
private void LogMetadata(string filename)
#pragma warning restore S1144
{
_logger.LogTrace("Metadata for {Path}:", filename);
@ -854,14 +874,11 @@ internal class PdfMetadataExtractor : IPdfMetadataExtractor
if (!_lexer.TestByte((byte)'x'))
{
// Cross-reference stream (PDF Spec 7.5.8)
ReadXRefStream();
return;
}
// Cross-reference table (PDF Spec 7.5.4)
var token = _lexer.NextToken();
if (token.Type != PdfLexer.TokenType.Keyword || (string)token.Value != "xref")
@ -885,23 +902,17 @@ internal class PdfMetadataExtractor : IPdfMetadataExtractor
var numObj = (long)token.Value;
if (_objectOffsets.Length < startObj + numObj)
{
Array.Resize(ref _objectOffsets, (int)(startObj + numObj));
}
_lexer.ExpectNewline();
var generation = 0;
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")
@ -1105,11 +1116,6 @@ internal class PdfMetadataExtractor : IPdfMetadataExtractor
{
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)
{
long type = 0;
@ -1136,9 +1142,9 @@ internal class PdfMetadataExtractor : IPdfMetadataExtractor
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();
//_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);
ReadMetadataFromXml(MetadataObjInObjectCatalog(meta.Root));
@ -1265,7 +1271,7 @@ internal class PdfMetadataExtractor : IPdfMetadataExtractor
// Document information dictionary (PDF Spec 14.3.3)
// We treat this as less authoritative than the Metadata stream.
if (infoObj < 1 || infoObj >= _objectOffsets.Length || _objectOffsets[infoObj] == 0)
if (!HasObject(infoObj))
{
return;
}
@ -1338,7 +1344,7 @@ internal class PdfMetadataExtractor : IPdfMetadataExtractor
{
// 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;
}
@ -1416,7 +1422,7 @@ internal class PdfMetadataExtractor : IPdfMetadataExtractor
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);
_lexer.ResetBuffer();
@ -1634,4 +1640,28 @@ internal class PdfMetadataExtractor : IPdfMetadataExtractor
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,10 +679,8 @@ public class BookService : IBookService
{
return _pdfComicInfoExtractor.GetComicInfo(filePath);
}
else
{
return GetEpubComicInfo(filePath);
}
return GetEpubComicInfo(filePath);
}
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="encodeFormat">Convert and save as encoding format</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>
string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = 320);
/// <param name="targetDirectory">If null, will write to <see cref="DirectoryService.CoverImageDirectory"/></param>
/// <returns>File name with extension of the file. </returns>
string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = 320, string? targetDirectory = null);
/// <summary>
/// Writes out a thumbnail by stream input
/// </summary>
@ -576,14 +577,16 @@ public class ImageService : IImageService
/// <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
try
{
targetDirectory ??= _directoryService.CoverImageDirectory;
using var thumbnail = Image.ThumbnailBuffer(Convert.FromBase64String(encodedImage), thumbnailWidth);
fileName += encodeFormat.GetExtension();
thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, fileName));
thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(targetDirectory, fileName));
return fileName;
}
catch (Exception e)

View File

@ -478,6 +478,8 @@ public class CoverDbService : ICoverDbService
var format = ImageService.GetPersonFormat(person.Id);
var finalFileName = format + ".webp";
var tempFileName = format + "_new";
// This is writing the image to CoverDirectory
var tempFilePath = await CreateThumbnail(url, tempFileName, fromBase64, tempDir);
if (!string.IsNullOrEmpty(tempFilePath))
@ -719,7 +721,7 @@ public class CoverDbService : ICoverDbService
/// <param name="url"></param>
/// <param name="filenameWithoutExtension">Filename without extension</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>
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)
{
return _imageService.CreateThumbnailFromBase64(url,
filenameWithoutExtension, encodeFormat, coverImageSize.GetDimensions().Width);
filenameWithoutExtension, encodeFormat, coverImageSize.GetDimensions().Width, targetDirectory);
}
return await DownloadImageFromUrl(filenameWithoutExtension, encodeFormat, url, targetDirectory);

View File

@ -118,83 +118,6 @@ public static partial class Parser
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 =
[
// Thai Volume: เล่ม n -> Volume n
@ -239,7 +162,7 @@ public static partial class Parser
RegexTimeout),
// Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA], Black Bullet - v4 c17 [batoto]
new Regex(
@"(?<Series>.+?)( - )(?:v|vo|c|chapters)\d",
@"(?<Series>.+?)( - )(?:v|vo|c|chapters|tome|t|ch)\d",
MatchOptions, RegexTimeout),
// Kedouin Makoto - Corpse Party Musume, Chapter 19 [Dametrans].zip
new Regex(
@ -251,14 +174,18 @@ public static partial class Parser
MatchOptions, RegexTimeout),
// [dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz
new Regex(
@"(?<Series>.+?):? (\b|_|-)(vol)\.?(\s|-|_)?\d+",
@"(?<Series>.+?):? (\b|_|-)(vol|tome)\.?(\s|-|_)?\d+",
MatchOptions, RegexTimeout),
// [xPearse] Kyochuu Rettou Chapter 001 Volume 1 [English] [Manga] [Volume Scans]
new Regex(
@"(?<Series>.+?):?(\s|\b|_|-)Chapter(\s|\b|_|-)\d+(\s|\b|_|-)(vol)(ume)",
MatchOptions,
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]
new Regex(
@"(?<Series>.+?):? (\b|_|-)(vol)(ume)",
@ -270,7 +197,7 @@ public static partial class Parser
MatchOptions, RegexTimeout),
//Tonikaku Cawaii [Volume 11], Darling in the FranXX - Volume 01.cbz
new Regex(
@"(?<Series>.*)(?: _|-|\[|\()\s?vol(ume)?",
@"(?<Series>.*)(?: _|-|\[|\()\s?(vol(ume)?|tome|t\d+)",
MatchOptions, RegexTimeout),
// Momo The Blood Taker - Chapter 027 Violent Emotion.cbz, Grand Blue Dreaming - SP02 Extra (2019) (Digital) (danke-Empire).cbz
new Regex(
@ -465,6 +392,83 @@ public static partial class Parser
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 =
[
// Thai Volume: เล่ม n -> Volume n

View File

@ -12,9 +12,9 @@
<PackageReference Include="Cronos" Version="0.11.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
<PackageReference Include="Flurl.Http" Version="4.0.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.6" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.11.0.117924">
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.7" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.15.0.120848">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</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 { of } from 'rxjs';
import {of} from 'rxjs';
import {filter, map, tap} from 'rxjs/operators';
import { environment } from 'src/environments/environment';
import { JumpKey } from '../_models/jumpbar/jump-key';
import { Library, LibraryType } from '../_models/library/library';
import { DirectoryDto } from '../_models/system/directory-dto';
import {environment} from 'src/environments/environment';
import {JumpKey} from '../_models/jumpbar/jump-key';
import {Library, LibraryType} from '../_models/library/library';
import {DirectoryDto} from '../_models/system/directory-dto';
import {EVENTS, MessageHubService} from "./message-hub.service";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@ -73,6 +73,10 @@ export class LibraryService {
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) {
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) {
return this.httpClient.post<Person>(this.baseUrl + 'person/merge', {destId, srcId});
}

View File

@ -10,13 +10,13 @@ import {
Output
} from '@angular/core';
import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
import {NgxFileDropEntry, FileSystemFileEntry, NgxFileDropModule} from 'ngx-file-drop';
import { fromEvent } from 'rxjs';
import { takeWhile } from 'rxjs/operators';
import { ToastrService } from 'ngx-toastr';
import { ImageService } from 'src/app/_services/image.service';
import { KEY_CODES } from 'src/app/shared/_services/utility.service';
import { UploadService } from 'src/app/_services/upload.service';
import {FileSystemFileEntry, NgxFileDropEntry, NgxFileDropModule} from 'ngx-file-drop';
import {fromEvent} from 'rxjs';
import {takeWhile} from 'rxjs/operators';
import {ToastrService} from 'ngx-toastr';
import {ImageService} from 'src/app/_services/image.service';
import {KEY_CODES} from 'src/app/shared/_services/utility.service';
import {UploadService} from 'src/app/_services/upload.service';
import {DOCUMENT, NgClass} from '@angular/common';
import {ImageComponent} from "../../shared/image/image.component";
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.selectedBase64Url.emit(e.target.result);
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();
}

View File

@ -73,7 +73,13 @@
<ng-template #view>
<input id="asin" class="form-control" formControlName="asin" type="text"
[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>
</app-setting-item>

View File

@ -73,7 +73,7 @@ export class EditPersonModalComponent implements OnInit {
editForm: FormGroup = new FormGroup({
name: new FormControl('', [Validators.required]),
description: new FormControl('', []),
asin: new FormControl('', []),
asin: new FormControl('', [], [this.asinValidator()]),
aniListId: new FormControl('', []),
malId: 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">
<a ngbNavLink>{{t(TabID.Folder)}}</a>
<ng-template ngbNavContent>
@if (filesAtRoot()) {
<p class="alert alert-warning">{{t('files-at-root-warning')}}</p>
}
<p>{{t('folder-description')}}</p>
<ul class="list-group list-group-flush" style="width: 100%">
@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>
</li>
}
</ul>
<div class="row mt-2">
<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 {
NgbActiveModal,
@ -13,7 +22,6 @@ import {
} from '@ng-bootstrap/ng-bootstrap';
import {ToastrService} from 'ngx-toastr';
import {debounceTime, distinctUntilChanged, switchMap, tap} from 'rxjs';
import {SettingsService} from 'src/app/admin/settings.service';
import {
DirectoryPickerComponent,
DirectoryPickerResult
@ -78,7 +86,6 @@ export class LibrarySettingsModalComponent implements OnInit {
private readonly destroyRef = inject(DestroyRef);
private readonly uploadService = inject(UploadService);
private readonly modalService = inject(NgbModal);
private readonly settingService = inject(SettingsService);
private readonly confirmService = inject(ConfirmService);
private readonly libraryService = inject(LibraryService);
private readonly toastr = inject(ToastrService);
@ -128,6 +135,7 @@ export class LibrarySettingsModalComponent implements OnInit {
setupStep = StepID.General;
fileTypeGroups = allFileTypeGroup;
excludePatterns: Array<string> = [''];
filesAtRoot = model<boolean>(false);
tasks: ActionItem<Library>[] = this.getTasks();
@ -145,6 +153,8 @@ export class LibrarySettingsModalComponent implements OnInit {
if (this.library === undefined) {
this.isAddLibrary = true;
this.cdRef.markForCheck();
} else {
this.checkForFilesAtRoot();
}
if (this.library?.coverImage != null && this.library?.coverImage !== '') {
@ -310,7 +320,14 @@ export class LibrarySettingsModalComponent implements OnInit {
}
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() {
@ -340,6 +357,7 @@ export class LibrarySettingsModalComponent implements OnInit {
}
model.excludePatterns = this.excludePatterns;
if (this.libraryForm.errors) {
return;
}
@ -402,6 +420,7 @@ export class LibrarySettingsModalComponent implements OnInit {
if (!this.selectedFolders.includes(closeResult.folderPath)) {
this.selectedFolders.push(closeResult.folderPath);
this.madeChanges = true;
this.checkForFilesAtRoot();
this.cdRef.markForCheck();
}
}
@ -411,6 +430,7 @@ export class LibrarySettingsModalComponent implements OnInit {
removeFolder(folder: string) {
this.selectedFolders = this.selectedFolders.filter(item => item !== folder);
this.madeChanges = true;
this.checkForFilesAtRoot();
this.cdRef.markForCheck();
}
@ -453,4 +473,18 @@ export class LibrarySettingsModalComponent implements OnInit {
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.",
"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.",
"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": {
@ -2272,6 +2273,7 @@
"hardcover-tooltip": "https://hardcover.app/authors/{HardcoverId}",
"asin-label": "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",
"required-field": "{{validations.required-field}}",
"cover-image-description": "{{edit-series-modal.cover-image-description}}",