mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-31 14:33:50 -04:00
Small Fixes (#3951)
This commit is contained in:
parent
152f7ad00e
commit
032b8f54b7
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
8
API/DTOs/CheckForFilesInFolderRootsDto.cs
Normal file
8
API/DTOs/CheckForFilesInFolderRootsDto.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace API.DTOs;
|
||||
|
||||
public sealed record CheckForFilesInFolderRootsDto
|
||||
{
|
||||
public ICollection<string> Roots { get; init; }
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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});
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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()">
|
||||
|
@ -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);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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}}",
|
||||
|
Loading…
x
Reference in New Issue
Block a user