Compare commits

...
Sign in to create a new pull request.

1 commit

Author SHA1 Message Date
Joseph Milazzo
a3809a668a Added an additional check to Manga volume/chapter parser to handle duplicate volume/chapter markers in the file:
"Series - Vol 1 Ch 2.5 - Vol 2 Omakes"

This will now be parsed correctly as Volume 1 Chapter 2.5.
2025-06-29 10:10:50 -05:00
2 changed files with 199 additions and 25 deletions

View file

@ -1,4 +1,5 @@
using API.Entities.Enums; using API.Entities.Enums;
using API.Services.Tasks.Scanner.Parser;
using Xunit; using Xunit;
namespace API.Tests.Parsing; namespace API.Tests.Parsing;
@ -17,7 +18,7 @@ public class MangaParsingTests
[InlineData("v001", "1")] [InlineData("v001", "1")]
[InlineData("Vol 1", "1")] [InlineData("Vol 1", "1")]
[InlineData("vol_356-1", "356")] // Mangapy syntax [InlineData("vol_356-1", "356")] // Mangapy syntax
[InlineData("No Volume", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)] [InlineData("No Volume", Parser.LooseLeafVolume)]
[InlineData("U12 (Under 12) Vol. 0001 Ch. 0001 - Reiwa Scans (gb)", "1")] [InlineData("U12 (Under 12) Vol. 0001 Ch. 0001 - Reiwa Scans (gb)", "1")]
[InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip", "1.1")] [InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip", "1.1")]
[InlineData("Tonikaku Cawaii [Volume 11].cbz", "11")] [InlineData("Tonikaku Cawaii [Volume 11].cbz", "11")]
@ -32,18 +33,18 @@ public class MangaParsingTests
[InlineData("Dorohedoro v01 (2010) (Digital) (LostNerevarine-Empire).cbz", "1")] [InlineData("Dorohedoro v01 (2010) (Digital) (LostNerevarine-Empire).cbz", "1")]
[InlineData("Dorohedoro v11 (2013) (Digital) (LostNerevarine-Empire).cbz", "11")] [InlineData("Dorohedoro v11 (2013) (Digital) (LostNerevarine-Empire).cbz", "11")]
[InlineData("Yumekui_Merry_v01_c01[Bakayarou-Kuu].rar", "1")] [InlineData("Yumekui_Merry_v01_c01[Bakayarou-Kuu].rar", "1")]
[InlineData("Yumekui-Merry_DKThias_Chapter11v2.zip", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)] [InlineData("Yumekui-Merry_DKThias_Chapter11v2.zip", Parser.LooseLeafVolume)]
[InlineData("Itoshi no Karin - c001-006x1 (v01) [Renzokusei Scans]", "1")] [InlineData("Itoshi no Karin - c001-006x1 (v01) [Renzokusei Scans]", "1")]
[InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 12", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)] [InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 12", Parser.LooseLeafVolume)]
[InlineData("VanDread-v01-c001[MD].zip", "1")] [InlineData("VanDread-v01-c001[MD].zip", "1")]
[InlineData("Ichiban_Ushiro_no_Daimaou_v04_ch27_[VISCANS].zip", "4")] [InlineData("Ichiban_Ushiro_no_Daimaou_v04_ch27_[VISCANS].zip", "4")]
[InlineData("Mob Psycho 100 v02 (2019) (Digital) (Shizu).cbz", "2")] [InlineData("Mob Psycho 100 v02 (2019) (Digital) (Shizu).cbz", "2")]
[InlineData("Kodomo no Jikan vol. 1.cbz", "1")] [InlineData("Kodomo no Jikan vol. 1.cbz", "1")]
[InlineData("Kodomo no Jikan vol. 10.cbz", "10")] [InlineData("Kodomo no Jikan vol. 10.cbz", "10")]
[InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 12 [Dametrans][v2]", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)] [InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 12 [Dametrans][v2]", Parser.LooseLeafVolume)]
[InlineData("Vagabond_v03", "3")] [InlineData("Vagabond_v03", "3")]
[InlineData("Mujaki No Rakune Volume 10.cbz", "10")] [InlineData("Mujaki No Rakune Volume 10.cbz", "10")]
[InlineData("Umineko no Naku Koro ni - Episode 3 - Banquet of the Golden Witch #02.cbz", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)] [InlineData("Umineko no Naku Koro ni - Episode 3 - Banquet of the Golden Witch #02.cbz", Parser.LooseLeafVolume)]
[InlineData("Volume 12 - Janken Boy is Coming!.cbz", "12")] [InlineData("Volume 12 - Janken Boy is Coming!.cbz", "12")]
[InlineData("[dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz", "20")] [InlineData("[dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz", "20")]
[InlineData("Gantz.V26.cbz", "26")] [InlineData("Gantz.V26.cbz", "26")]
@ -52,7 +53,7 @@ public class MangaParsingTests
[InlineData("NEEDLESS_Vol.4_-_Simeon_6_v2_[SugoiSugoi].rar", "4")] [InlineData("NEEDLESS_Vol.4_-_Simeon_6_v2_[SugoiSugoi].rar", "4")]
[InlineData("Okusama wa Shougakusei c003 (v01) [bokuwaNEET]", "1")] [InlineData("Okusama wa Shougakusei c003 (v01) [bokuwaNEET]", "1")]
[InlineData("Sword Art Online Vol 10 - Alicization Running [Yen Press] [LuCaZ] {r2}.epub", "10")] [InlineData("Sword Art Online Vol 10 - Alicization Running [Yen Press] [LuCaZ] {r2}.epub", "10")]
[InlineData("Noblesse - Episode 406 (52 Pages).7z", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)] [InlineData("Noblesse - Episode 406 (52 Pages).7z", Parser.LooseLeafVolume)]
[InlineData("X-Men v1 #201 (September 2007).cbz", "1")] [InlineData("X-Men v1 #201 (September 2007).cbz", "1")]
[InlineData("Hentai Ouji to Warawanai Neko. - Vol. 06 Ch. 034.5", "6")] [InlineData("Hentai Ouji to Warawanai Neko. - Vol. 06 Ch. 034.5", "6")]
[InlineData("The 100 Girlfriends Who Really, Really, Really, Really, Really Love You - Vol. 03 Ch. 023.5 - Volume 3 Extras.cbz", "3")] [InlineData("The 100 Girlfriends Who Really, Really, Really, Really, Really Love You - Vol. 03 Ch. 023.5 - Volume 3 Extras.cbz", "3")]
@ -64,7 +65,7 @@ public class MangaParsingTests
[InlineData("スライム倒して300年、知らないうちにレベルMAXになってました 1-3巻", "1-3")] [InlineData("スライム倒して300年、知らないうちにレベルMAXになってました 1-3巻", "1-3")]
[InlineData("Dance in the Vampire Bund {Special Edition} v03.5 (2019) (Digital) (KG Manga)", "3.5")] [InlineData("Dance in the Vampire Bund {Special Edition} v03.5 (2019) (Digital) (KG Manga)", "3.5")]
[InlineData("Kebab Том 1 Глава 3", "1")] [InlineData("Kebab Том 1 Глава 3", "1")]
[InlineData("Манга Глава 2", API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume)] [InlineData("Манга Глава 2", Parser.LooseLeafVolume)]
[InlineData("Манга Тома 1-4", "1-4")] [InlineData("Манга Тома 1-4", "1-4")]
[InlineData("Манга Том 1-4", "1-4")] [InlineData("Манга Том 1-4", "1-4")]
[InlineData("조선왕조실톡 106화", "106")] [InlineData("조선왕조실톡 106화", "106")]
@ -72,13 +73,54 @@ public class MangaParsingTests
[InlineData("몰?루 아카이브 7.5권", "7.5")] [InlineData("몰?루 아카이브 7.5권", "7.5")]
[InlineData("63권#200", "63")] [InlineData("63권#200", "63")]
[InlineData("시즌34삽화2", "34")] [InlineData("시즌34삽화2", "34")]
[InlineData("시즌3-4삽화2", "3-4")]
[InlineData("Accel World Chapter 001 Volume 002", "2")] [InlineData("Accel World Chapter 001 Volume 002", "2")]
[InlineData("Accel World Volume 2", "2")] [InlineData("Accel World Volume 2", "2")]
[InlineData("Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.31 Omake", "30")] [InlineData("Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.31 Omake", "30")]
[InlineData("Zom 100 - Bucket List of the Dead v01", "1")] [InlineData("Zom 100 - Bucket List of the Dead v01", "1")]
public void ParseVolumeTest(string filename, string expected) public void ParseVolumeTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename, LibraryType.Manga)); Assert.Equal(expected, Parser.ParseVolume(filename, LibraryType.Manga));
}
[Theory]
[InlineData("One Piece - Vol 2 Ch 1.1 - Volume 4 Omakes", "2")]
[InlineData("Attack on Titan - Vol. 5 Ch. 20 - Vol.10 Special", "5")]
[InlineData("Naruto - Volume 1 Chapter 1 - Volume 2 Preview", "1")]
[InlineData("My Hero Academia - Vol 15 - Vol 20 Extras", "15")]
// Edge cases for duplicate detection
[InlineData("Series - Vol 1 - Not Vol but Voldemort", "1")] // Should not trigger false positive
[InlineData("Volume Wars - Vol 1 vs Vol 2", "1")] // Series name contains "Volume"
[InlineData("Vol 3 - The Volume Chronicles - Vol 5", "3")] // Multiple volume references
// Thai Volume tests
[InlineData("เล่ม 5 - Chapter 1", "5")]
[InlineData("เล่มที่ 12 Test", "12")]
// Chinese Volume tests
[InlineData("幽游白书完全版 第03卷 天下", "3")]
[InlineData("阿衰online 第1册", "1")]
[InlineData("卷5 Test", "5")]
[InlineData("册10 Test", "10")]
// Korean Volume tests
[InlineData("제5권 Test", "5")]
[InlineData("10화 Test", "10")]
[InlineData("시즌3 Test", "3")]
[InlineData("5시즌 Test", Parser.LooseLeafVolume)]
// Japanese Volume tests
[InlineData("Test 5巻", "5")]
[InlineData("Series 10-15巻", "10-15")]
// Russian Volume tests
[InlineData("Том 5 Test", "5")]
[InlineData("Тома 10 Test", "10")]
[InlineData("5 Том Test", "5")]
public void ParseDuplicateVolumeTest(string filename, string expected)
{
Assert.Equal(expected, Parser.ParseVolume(filename, LibraryType.Manga));
} }
[Theory] [Theory]
@ -208,19 +250,19 @@ public class MangaParsingTests
[InlineData("Zom 100 - Bucket List of the Dead v01", "Zom 100 - Bucket List of the Dead")] [InlineData("Zom 100 - Bucket List of the Dead v01", "Zom 100 - Bucket List of the Dead")]
public void ParseSeriesTest(string filename, string expected) public void ParseSeriesTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename, LibraryType.Manga)); Assert.Equal(expected, Parser.ParseSeries(filename, LibraryType.Manga));
} }
[Theory] [Theory]
[InlineData("Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)", "1")] [InlineData("Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)", "1")]
[InlineData("My Girlfriend Is Shobitch v01 - ch. 09 - pg. 008.png", "9")] [InlineData("My Girlfriend Is Shobitch v01 - ch. 09 - pg. 008.png", "9")]
[InlineData("Historys Strongest Disciple Kenichi_v11_c90-98.zip", "90-98")] [InlineData("Historys Strongest Disciple Kenichi_v11_c90-98.zip", "90-98")]
[InlineData("B_Gata_H_Kei_v01[SlowManga&OverloadScans]", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] [InlineData("B_Gata_H_Kei_v01[SlowManga&OverloadScans]", Parser.DefaultChapter)]
[InlineData("BTOOOM! v01 (2013) (Digital) (Shadowcat-Empire)", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] [InlineData("BTOOOM! v01 (2013) (Digital) (Shadowcat-Empire)", Parser.DefaultChapter)]
[InlineData("Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA]", "1-8")] [InlineData("Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA]", "1-8")]
[InlineData("Dance in the Vampire Bund v16-17 (Digital) (NiceDragon)", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] [InlineData("Dance in the Vampire Bund v16-17 (Digital) (NiceDragon)", Parser.DefaultChapter)]
[InlineData("c001", "1")] [InlineData("c001", "1")]
[InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.12.zip", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] [InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.12.zip", Parser.DefaultChapter)]
[InlineData("Adding volume 1 with File: Ana Satsujin Vol. 1 Ch. 5 - Manga Box (gb).cbz", "5")] [InlineData("Adding volume 1 with File: Ana Satsujin Vol. 1 Ch. 5 - Manga Box (gb).cbz", "5")]
[InlineData("Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz", "18")] [InlineData("Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz", "18")]
[InlineData("Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip", "0-6")] [InlineData("Cynthia The Mission - c000-006 (v06) [Desudesu&Brolen].zip", "0-6")]
@ -243,7 +285,7 @@ public class MangaParsingTests
[InlineData("Itoshi no Karin - c001-006x1 (v01) [Renzokusei Scans]", "1-6")] [InlineData("Itoshi no Karin - c001-006x1 (v01) [Renzokusei Scans]", "1-6")]
[InlineData("APOSIMZ 040 (2020) (Digital) (danke-Empire).cbz", "40")] [InlineData("APOSIMZ 040 (2020) (Digital) (danke-Empire).cbz", "40")]
[InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 12", "12")] [InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 12", "12")]
[InlineData("Vol 1", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] [InlineData("Vol 1", Parser.DefaultChapter)]
[InlineData("VanDread-v01-c001[MD].zip", "1")] [InlineData("VanDread-v01-c001[MD].zip", "1")]
[InlineData("Goblin Slayer Side Story - Year One 025.5", "25.5")] [InlineData("Goblin Slayer Side Story - Year One 025.5", "25.5")]
[InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 01", "1")] [InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 01", "1")]
@ -255,10 +297,10 @@ public class MangaParsingTests
[InlineData("Fullmetal Alchemist chapters 101-108.cbz", "101-108")] [InlineData("Fullmetal Alchemist chapters 101-108.cbz", "101-108")]
[InlineData("Umineko no Naku Koro ni - Episode 3 - Banquet of the Golden Witch #02.cbz", "2")] [InlineData("Umineko no Naku Koro ni - Episode 3 - Banquet of the Golden Witch #02.cbz", "2")]
[InlineData("To Love Ru v09 Uncensored (Ch.071-079).cbz", "71-79")] [InlineData("To Love Ru v09 Uncensored (Ch.071-079).cbz", "71-79")]
[InlineData("Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Extra Chapter.rar", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] [InlineData("Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Extra Chapter.rar", Parser.DefaultChapter)]
[InlineData("Beelzebub_153b_RHS.zip", "153.5")] [InlineData("Beelzebub_153b_RHS.zip", "153.5")]
[InlineData("Beelzebub_150-153b_RHS.zip", "150-153.5")] [InlineData("Beelzebub_150-153b_RHS.zip", "150-153.5")]
[InlineData("Transferred to another world magical swordsman v1.1", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] [InlineData("Transferred to another world magical swordsman v1.1", Parser.DefaultChapter)]
[InlineData("Kiss x Sis - Ch.15 - The Angst of a 15 Year Old Boy.cbz", "15")] [InlineData("Kiss x Sis - Ch.15 - The Angst of a 15 Year Old Boy.cbz", "15")]
[InlineData("Kiss x Sis - Ch.12 - 1 , 2 , 3P!.cbz", "12")] [InlineData("Kiss x Sis - Ch.12 - 1 , 2 , 3P!.cbz", "12")]
[InlineData("Umineko no Naku Koro ni - Episode 1 - Legend of the Golden Witch #1", "1")] [InlineData("Umineko no Naku Koro ni - Episode 1 - Legend of the Golden Witch #1", "1")]
@ -277,21 +319,21 @@ public class MangaParsingTests
[InlineData("Kimi no Koto ga Daidaidaidaidaisuki na 100-nin no Kanojo Chapter 1-10", "1-10")] [InlineData("Kimi no Koto ga Daidaidaidaidaisuki na 100-nin no Kanojo Chapter 1-10", "1-10")]
[InlineData("Deku_&_Bakugo_-_Rising_v1_c1.1.cbz", "1.1")] [InlineData("Deku_&_Bakugo_-_Rising_v1_c1.1.cbz", "1.1")]
[InlineData("Chapter 63 - The Promise Made for 520 Cenz.cbr", "63")] [InlineData("Chapter 63 - The Promise Made for 520 Cenz.cbr", "63")]
[InlineData("Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] [InlineData("Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub", Parser.DefaultChapter)]
[InlineData("Kaiju No. 8 036 (2021) (Digital)", "36")] [InlineData("Kaiju No. 8 036 (2021) (Digital)", "36")]
[InlineData("Samurai Jack Vol. 01 - The threads of Time", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] [InlineData("Samurai Jack Vol. 01 - The threads of Time", Parser.DefaultChapter)]
[InlineData("【TFO汉化&Petit汉化】迷你偶像漫画第25话", "25")] [InlineData("【TFO汉化&Petit汉化】迷你偶像漫画第25话", "25")]
[InlineData("자유록 13회#2", "13")] [InlineData("자유록 13회#2", "13")]
[InlineData("이세계에서 고아원을 열었지만, 어째서인지 아무도 독립하려 하지 않는다 38-1화 ", "38")] [InlineData("이세계에서 고아원을 열었지만, 어째서인지 아무도 독립하려 하지 않는다 38-1화 ", "38")]
[InlineData("[ハレム] SMごっこ 10", "10")] [InlineData("[ハレム] SMごっこ 10", "10")]
[InlineData("Dance in the Vampire Bund {Special Edition} v03.5 (2019) (Digital) (KG Manga)", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] [InlineData("Dance in the Vampire Bund {Special Edition} v03.5 (2019) (Digital) (KG Manga)", Parser.DefaultChapter)]
[InlineData("Kebab Том 1 Глава 3", "3")] [InlineData("Kebab Том 1 Глава 3", "3")]
[InlineData("Манга Глава 2", "2")] [InlineData("Манга Глава 2", "2")]
[InlineData("Манга 2 Глава", "2")] [InlineData("Манга 2 Глава", "2")]
[InlineData("Манга Том 1 2 Глава", "2")] [InlineData("Манга Том 1 2 Глава", "2")]
[InlineData("Accel World Chapter 001 Volume 002", "1")] [InlineData("Accel World Chapter 001 Volume 002", "1")]
[InlineData("Bleach 001-003", "1-3")] [InlineData("Bleach 001-003", "1-3")]
[InlineData("Accel World Volume 2", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter)] [InlineData("Accel World Volume 2", Parser.DefaultChapter)]
[InlineData("Historys Strongest Disciple Kenichi_v11_c90-98", "90-98")] [InlineData("Historys Strongest Disciple Kenichi_v11_c90-98", "90-98")]
[InlineData("Historys Strongest Disciple Kenichi c01-c04", "1-4")] [InlineData("Historys Strongest Disciple Kenichi c01-c04", "1-4")]
[InlineData("Adabana c00-02", "0-2")] [InlineData("Adabana c00-02", "0-2")]
@ -301,7 +343,36 @@ public class MangaParsingTests
[InlineData("Monster #8 Ch. 001", "1")] [InlineData("Monster #8 Ch. 001", "1")]
public void ParseChaptersTest(string filename, string expected) public void ParseChaptersTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename, LibraryType.Manga)); Assert.Equal(expected, Parser.ParseChapter(filename, LibraryType.Manga));
}
/// <summary>
/// Handles edge case testing around duplicate numbers in the filename
/// </summary>
[Theory]
[InlineData("Manga Title - Ch.1 - The 22 beers", "1")]
public void ParseExtraNumberChaptersTest(string filename, string expected)
{
Assert.Equal(expected, Parser.ParseChapter(filename, LibraryType.Manga));
}
[Theory]
[InlineData("Manga Title - Ch.1 Part.A - Ch.2 Omake", "1")]
[InlineData("Another Series - Chapter 10 Something - Chapter 15 Extra", "10")]
[InlineData("Test_Ch_3_Content_Ch_7_Bonus", "3")]
[InlineData("One Piece - Ch 5 Part 1 - Chapter 10 Omakes", "5")]
[InlineData("Attack on Titan - Chapter 20 Content - Ch 25 Special", "20")]
[InlineData("Naruto - Ch. 1 Story - Ch. 5 Preview", "1")]
[InlineData("My Hero Academia - Chapter 15 - Chapter 20 Extras", "15")]
[InlineData("Series Name - c2 Content - c5 Bonus", "2")]
[InlineData("Test Series - c1 Part1 - Chapter 3 Extra", "1")]
[InlineData("Another Test - Chapter 7 - c10 Omake", "7")]
[InlineData("Series - Ch 1 - Not Ch but Chaos", "1")]
[InlineData("Chapter Wars - Ch 1 vs Ch 2", "1")]
[InlineData("Ch 3 - The Chapter Chronicles - Ch 5", "3")]
public void ParseDuplicateChapterTest(string filename, string expected)
{
Assert.Equal(expected, Parser.ParseChapter(filename, LibraryType.Manga));
} }
@ -318,7 +389,7 @@ public class MangaParsingTests
[InlineData("Love Hina Omnibus v05 (2015) (Digital-HD) (Asgard-Empire).cbz", "Omnibus")] [InlineData("Love Hina Omnibus v05 (2015) (Digital-HD) (Asgard-Empire).cbz", "Omnibus")]
public void ParseEditionTest(string input, string expected) public void ParseEditionTest(string input, string expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseEdition(input)); Assert.Equal(expected, Parser.ParseEdition(input));
} }
[Theory] [Theory]
[InlineData("Beelzebub Special OneShot - Minna no Kochikame x Beelzebub (2016) [Mangastream].cbz", false)] [InlineData("Beelzebub Special OneShot - Minna no Kochikame x Beelzebub (2016) [Mangastream].cbz", false)]
@ -339,7 +410,7 @@ public class MangaParsingTests
[InlineData("Hajime no Ippo - Artbook", false)] [InlineData("Hajime no Ippo - Artbook", false)]
public void IsMangaSpecialTest(string input, bool expected) public void IsMangaSpecialTest(string input, bool expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.IsSpecial(input, LibraryType.Manga)); Assert.Equal(expected, Parser.IsSpecial(input, LibraryType.Manga));
} }
[Theory] [Theory]
@ -348,7 +419,7 @@ public class MangaParsingTests
[InlineData("image.txt", MangaFormat.Unknown)] [InlineData("image.txt", MangaFormat.Unknown)]
public void ParseFormatTest(string inputFile, MangaFormat expected) public void ParseFormatTest(string inputFile, MangaFormat expected)
{ {
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseFormat(inputFile)); Assert.Equal(expected, Parser.ParseFormat(inputFile));
} }

View file

@ -117,6 +117,36 @@ public static partial class Parser
private static readonly Regex SpecialTokenRegex = new(@"SP\d+", private static readonly Regex SpecialTokenRegex = new(@"SP\d+",
MatchOptions, RegexTimeout); MatchOptions, RegexTimeout);
/// <summary>
/// An additional check to avoid situations like "One Piece - Vol 4 ch 2 - vol 6 omakes"
/// </summary>
private static readonly Regex DuplicateVolumeRegex = new Regex(
@"(?i)(vol\.?|volume|v)(\s|_)*\d+.*?(vol\.?|volume|v)(\s|_)*\d+",
MatchOptions, RegexTimeout);
private static readonly Regex DuplicateChapterRegex = new Regex(
@"(?i)(ch\.?|chapter|c)(\s|_)*\d+.*?(ch\.?|chapter|c)(\s|_)*\d+",
MatchOptions, RegexTimeout);
// Regex to detect range patterns that should NOT be treated as duplicates (History's Strongest c1-c4)
private static readonly Regex VolumeRangeRegex = new Regex(
@"(vol\.?|v)(\s|_)?\d+(\.\d+)?-(vol\.?|v)(\s|_)?\d+(\.\d+)?",
MatchOptions, RegexTimeout);
private static readonly Regex ChapterRangeRegex = new Regex(
@"(ch\.?|c)(\s|_)?\d+(\.\d+)?-(ch\.?|c)(\s|_)?\d+(\.\d+)?",
MatchOptions, RegexTimeout);
// Regex to find volume number after a volume marker
private static readonly Regex VolumeNumberRegex = new Regex(
@"(vol\.?|volume|v)(\s|_)*(?<Volume>\d+(\.\d+)?(-\d+(\.\d+)?)?)",
MatchOptions, RegexTimeout);
// Regex to find chapter number after a chapter marker
private static readonly Regex ChapterNumberRegex = new Regex(
@"(ch\.?|chapter|c)(\s|_)*(?<Chapter>\d+(\.\d+)?(-\d+(\.\d+)?)?)",
MatchOptions, RegexTimeout);
private static readonly Regex[] MangaVolumeRegex = private static readonly Regex[] MangaVolumeRegex =
[ [
@ -171,7 +201,7 @@ public static partial class Parser
MatchOptions, RegexTimeout), MatchOptions, RegexTimeout),
// Korean Season: 시즌n -> Season n, // Korean Season: 시즌n -> Season n,
new Regex( new Regex(
@"시즌(?<Volume>\d+\-?\d+)", @"시즌(?<Volume>\d+(\-\d+)?)",
MatchOptions, RegexTimeout), MatchOptions, RegexTimeout),
// Korean Season: 시즌n -> Season n, n시즌 -> season n // Korean Season: 시즌n -> Season n, n시즌 -> season n
new Regex( new Regex(
@ -741,6 +771,8 @@ public static partial class Parser
public static string ParseMangaVolume(string filename) public static string ParseMangaVolume(string filename)
{ {
filename = RemoveDuplicateVolumeIfExists(filename);
foreach (var regex in MangaVolumeRegex) foreach (var regex in MangaVolumeRegex)
{ {
var matches = regex.Matches(filename); var matches = regex.Matches(filename);
@ -841,6 +873,8 @@ public static partial class Parser
private static string ParseMangaChapter(string filename) private static string ParseMangaChapter(string filename)
{ {
filename = RemoveDuplicateChapterIfExists(filename);
foreach (var regex in MangaChapterRegex) foreach (var regex in MangaChapterRegex)
{ {
var matches = regex.Matches(filename); var matches = regex.Matches(filename);
@ -1185,6 +1219,75 @@ public static partial class Parser
return filename; return filename;
} }
/// <summary>
/// Checks for a duplicate volume marker and removes it
/// </summary>
/// <param name="filename"></param>
/// <returns></returns>
private static string RemoveDuplicateVolumeIfExists(string filename)
{
// First check if this contains a volume range pattern - if so, don't process as duplicate (v1-v2, edge case)
if (VolumeRangeRegex.IsMatch(filename))
return filename;
var duplicateMatch = DuplicateVolumeRegex.Match(filename);
if (!duplicateMatch.Success) return filename;
// Find the start position of the first volume marker
var firstVolumeStart = duplicateMatch.Groups[1].Index;
// Find the volume number after the first marker
var volumeNumberMatch = VolumeNumberRegex.Match(filename, firstVolumeStart);
if (!volumeNumberMatch.Success) return filename;
var volumeNumberEnd = volumeNumberMatch.Index + volumeNumberMatch.Length;
// Find the second volume marker after the first volume number
var secondVolumeMatch = VolumeNumberRegex.Match(filename, volumeNumberEnd);
if (secondVolumeMatch.Success)
{
// Truncate the filename at the second volume marker
return filename.Substring(0, secondVolumeMatch.Index).TrimEnd(' ', '-', '_');
}
return filename;
}
/// <summary>
/// Removes duplicate chapter markers from filename, keeping only the first occurrence
/// </summary>
/// <param name="filename">Original filename</param>
/// <returns>Processed filename with duplicate chapter markers removed</returns>
public static string RemoveDuplicateChapterIfExists(string filename)
{
// First check if this contains a chapter range pattern - if so, don't process as duplicate (c1-c2, edge case)
if (ChapterRangeRegex.IsMatch(filename))
return filename;
var duplicateMatch = DuplicateChapterRegex.Match(filename);
if (!duplicateMatch.Success) return filename;
// Find the start position of the first chapter marker
var firstChapterStart = duplicateMatch.Groups[1].Index;
// Find the chapter number after the first marker
var chapterNumberMatch = ChapterNumberRegex.Match(filename, firstChapterStart);
if (!chapterNumberMatch.Success) return filename;
var chapterNumberEnd = chapterNumberMatch.Index + chapterNumberMatch.Length;
// Find the second chapter marker after the first chapter number
var secondChapterMatch = ChapterNumberRegex.Match(filename, chapterNumberEnd);
if (secondChapterMatch.Success)
{
// Truncate the filename at the second chapter marker
return filename.Substring(0, secondChapterMatch.Index).TrimEnd(' ', '-', '_');
}
return filename;
}
[GeneratedRegex(SupportedExtensions)] [GeneratedRegex(SupportedExtensions)]
private static partial Regex SupportedExtensionsRegex(); private static partial Regex SupportedExtensionsRegex();
[GeneratedRegex(@"\d-{1}\d")] [GeneratedRegex(@"\d-{1}\d")]