Merge branch 'develop' of https://github.com/Kareadita/Kavita into develop

This commit is contained in:
Joseph Milazzo 2021-03-29 15:22:38 -05:00
commit b709da3854
6 changed files with 221 additions and 45 deletions

View File

@ -53,6 +53,12 @@ namespace API.Tests
[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]", "0")] [InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 12 [Dametrans][v2]", "0")]
[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", "3")]
[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")]
public void ParseVolumeTest(string filename, string expected) public void ParseVolumeTest(string filename, string expected)
{ {
Assert.Equal(expected, ParseVolume(filename)); Assert.Equal(expected, ParseVolume(filename));
@ -105,6 +111,21 @@ namespace API.Tests
[InlineData("Goblin Slayer Side Story - Year One 025.5", "Goblin Slayer Side Story - Year One")] [InlineData("Goblin Slayer Side Story - Year One 025.5", "Goblin Slayer Side Story - Year One")]
[InlineData("Goblin Slayer - Brand New Day 006.5 (2019) (Digital) (danke-Empire)", "Goblin Slayer - Brand New Day")] [InlineData("Goblin Slayer - Brand New Day 006.5 (2019) (Digital) (danke-Empire)", "Goblin Slayer - Brand New Day")]
[InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 01 [Dametrans][v2]", "Kedouin Makoto - Corpse Party Musume")] [InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 01 [Dametrans][v2]", "Kedouin Makoto - Corpse Party Musume")]
[InlineData("Vagabond_v03", "Vagabond")]
[InlineData("[AN] Mahoutsukai to Deshi no Futekisetsu na Kankei Chp. 1", "Mahoutsukai to Deshi no Futekisetsu na Kankei")]
[InlineData("Beelzebub_Side_Story_02_RHS.zip", "Beelzebub Side Story")]
[InlineData("[BAA]_Darker_than_Black_Omake-1.zip", "Darker than Black")]
[InlineData("Baketeriya ch01-05.zip", "Baketeriya")]
[InlineData("[PROzess]Kimi_ha_midara_na_Boku_no_Joou_-_Ch01", "Kimi ha midara na Boku no Joou")]
[InlineData("[SugoiSugoi]_NEEDLESS_Vol.2_-_Disk_The_Informant_5_[ENG].rar", "NEEDLESS")]
[InlineData("Fullmetal Alchemist chapters 101-108.cbz", "Fullmetal Alchemist")]
[InlineData("To Love Ru v09 Uncensored (Ch.071-079).cbz", "To Love Ru")]
[InlineData("[dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz", "One Piece")]
//[InlineData("Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Extra Chapter", "Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U")]
[InlineData("Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Chapter 01", "Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U")]
[InlineData("Vol03_ch15-22.rar", "")]
[InlineData("Love Hina - Special.cbz", "")] // This has to be a fallback case
[InlineData("Ani-Hina Art Collection.cbz", "")] // This has to be a fallback case
public void ParseSeriesTest(string filename, string expected) public void ParseSeriesTest(string filename, string expected)
{ {
Assert.Equal(expected, ParseSeries(filename)); Assert.Equal(expected, ParseSeries(filename));
@ -148,6 +169,13 @@ namespace API.Tests
[InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 01", "1")] [InlineData("Kedouin Makoto - Corpse Party Musume, Chapter 01", "1")]
[InlineData("To Love Ru v11 Uncensored (Ch.089-097+Omake)", "89-97")] [InlineData("To Love Ru v11 Uncensored (Ch.089-097+Omake)", "89-97")]
[InlineData("To Love Ru v18 Uncensored (Ch.153-162.5)", "153-162.5")] [InlineData("To Love Ru v18 Uncensored (Ch.153-162.5)", "153-162.5")]
[InlineData("[AN] Mahoutsukai to Deshi no Futekisetsu na Kankei Chp. 1", "1")]
[InlineData("Beelzebub_Side_Story_02_RHS.zip", "2")]
[InlineData("[PROzess]Kimi_ha_midara_na_Boku_no_Joou_-_Ch01", "1")]
[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", "0")]
public void ParseChaptersTest(string filename, string expected) public void ParseChaptersTest(string filename, string expected)
{ {
Assert.Equal(expected, ParseChapter(filename)); Assert.Equal(expected, ParseChapter(filename));
@ -199,10 +227,27 @@ namespace API.Tests
[InlineData("Tenjou Tenge Omnibus", "Omnibus")] [InlineData("Tenjou Tenge Omnibus", "Omnibus")]
[InlineData("Tenjou Tenge {Full Contact Edition}", "Full Contact Edition")] [InlineData("Tenjou Tenge {Full Contact Edition}", "Full Contact Edition")]
[InlineData("Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz", "Full Contact Edition")] [InlineData("Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz", "Full Contact Edition")]
[InlineData("Wotakoi - Love is Hard for Otaku Omnibus v01 (2018) (Digital) (danke-Empire)", "Omnibus")]
[InlineData("To Love Ru v01 Uncensored (Ch.001-007)", "Uncensored")]
[InlineData("Chobits Omnibus Edition v01 [Dark Horse]", "Omnibus Edition")]
[InlineData("[dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz", "Digital Colored Comics")]
[InlineData("AKIRA - c003 (v01) [Full Color] [Darkhorse].cbz", "Full Color")]
public void ParseEditionTest(string input, string expected) public void ParseEditionTest(string input, string expected)
{ {
Assert.Equal(expected, ParseEdition(input)); Assert.Equal(expected, ParseEdition(input));
} }
[Theory]
[InlineData("Beelzebub Special OneShot - Minna no Kochikame x Beelzebub (2016) [Mangastream].cbz", true)]
[InlineData("Beelzebub_Omake_June_2012_RHS", true)]
[InlineData("Beelzebub_Side_Story_02_RHS.zip", false)]
[InlineData("Darker than Black Shikkoku no Hana Special [Simple Scans].zip", true)]
[InlineData("Darker than Black Shikkoku no Hana Fanbook Extra [Simple Scans].zip", true)]
[InlineData("Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Extra Chapter", true)]
[InlineData("Ani-Hina Art Collection.cbz", true)]
public void ParseMangaSpecialTest(string input, bool expected)
{
Assert.Equal(expected, ParseMangaSpecial(input) != "");
}
[Theory] [Theory]
[InlineData("12-14", 12)] [InlineData("12-14", 12)]

View File

@ -14,8 +14,7 @@ namespace API.Parser
private static readonly Regex ImageRegex = new Regex(ImageFileExtensions, RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex ImageRegex = new Regex(ImageFileExtensions, RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex MangaFileRegex = new Regex(MangaFileExtensions, RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex MangaFileRegex = new Regex(MangaFileExtensions, RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex XmlRegex = new Regex(XmlRegexExtensions, RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex XmlRegex = new Regex(XmlRegexExtensions, RegexOptions.IgnoreCase | RegexOptions.Compiled);
//?: is a non-capturing group in C#, else anything in () will be a group
private static readonly Regex[] MangaVolumeRegex = new[] private static readonly Regex[] MangaVolumeRegex = new[]
{ {
// Dance in the Vampire Bund v16-17 // Dance in the Vampire Bund v16-17
@ -32,17 +31,20 @@ namespace API.Parser
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb) // Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)
new Regex( new Regex(
@"(vol\.? ?)(?<Volume>0*[1-9]+)", @"(vol\.? ?)(?<Volume>\d+)",
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Tonikaku Cawaii [Volume 11].cbz // Tonikaku Cawaii [Volume 11].cbz
new Regex( new Regex(
@"(volume )(?<Volume>0?[1-9]+)", @"(volume )(?<Volume>\d+)",
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Tower Of God S01 014 (CBT) (digital).cbz // Tower Of God S01 014 (CBT) (digital).cbz
new Regex( new Regex(
@"(?<Series>.*)(\b|_|)(S(?<Volume>\d+))", @"(?<Series>.*)(\b|_|)(S(?<Volume>\d+))",
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Umineko no Naku Koro ni - Episode 3 - Banquet of the Golden Witch #02.cbz
new Regex(
@"(?<Series>.*)( |_|-)(?:Episode)(?: |_)(?<Volume>\d+(-\d+)?)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
}; };
@ -56,6 +58,10 @@ namespace API.Parser
new Regex( new Regex(
@"(?<Series>.*)( - )(?:v|vo|c)\d", @"(?<Series>.*)( - )(?:v|vo|c)\d",
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
// [dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz
new Regex(
@"(?<Series>.*) (\b|_|-)(vol)\.?",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Historys Strongest Disciple Kenichi_v11_c90-98.zip, Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb) // Historys Strongest Disciple Kenichi_v11_c90-98.zip, Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)
new Regex( new Regex(
@"(?<Series>.*) (\b|_|-)v", @"(?<Series>.*) (\b|_|-)v",
@ -97,17 +103,41 @@ namespace API.Parser
new Regex( new Regex(
@"(?<Series>.*)( |_)\((c |ch |chapter )", @"(?<Series>.*)( |_)\((c |ch |chapter )",
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Black Bullet (This is very loose, keep towards bottom) (?<Series>.*)(_)(v|vo|c|volume) // Black Bullet (This is very loose, keep towards bottom)
new Regex( new Regex(
@"(?<Series>.*)(_)(v|vo|c|volume)( |_)\d+", @"(?<Series>.*)(_)(v|vo|c|volume)( |_)\d+",
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Akiiro Bousou Biyori - 01.jpg, Beelzebub_172_RHS.zip, Cynthia the Mission 29.rar // Mahoutsukai to Deshi no Futekisetsu na Kankei Chp. 1
new Regex( new Regex(
@"^(?!Vol)(?<Series>.*)( |_)(\d+)", @"(?<Series>.*)( |_)(?:Chp.? ?\d+)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Chapter 01
new Regex(
@"^(?!Vol)(?<Series>.*)( |_)Chapter( |_)(\d+)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// [SugoiSugoi]_NEEDLESS_Vol.2_-_Disk_The_Informant_5_[ENG].rar
new Regex(
@"^(?<Series>.*)( |_)Vol\.?\d+",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Fullmetal Alchemist chapters 101-108.cbz
new Regex(
@"^(?!vol)(?<Series>.*)( |_)(chapters( |_)?)\d+-?\d*",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Baketeriya ch01-05.zip, Akiiro Bousou Biyori - 01.jpg, Beelzebub_172_RHS.zip, Cynthia the Mission 29.rar
new Regex(
@"^(?!Vol\.?)(?<Series>.*)( |_|-)(?<!-)(ch)?\d+-?\d*", //fails on
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Baketeriya ch01-05.zip
new Regex(
@"^(?!Vol)(?<Series>.*)ch\d+-?\d?",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// [BAA]_Darker_than_Black_Omake-1.zip
new Regex(
@"^(?!Vol)(?<Series>.*)(-)\d+-?\d*", // This catches a lot of stuff ^(?!Vol)(?<Series>.*)( |_)(\d+)
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
// [BAA]_Darker_than_Black_c1 (This is very greedy, make sure it's close to last) // [BAA]_Darker_than_Black_c1 (This is very greedy, make sure it's close to last)
new Regex( new Regex(
@"(?<Series>.*)( |_)(c)\d+", @"^(?!Vol)(?<Series>.*)( |_|-)(ch?)\d+",
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
}; };
@ -123,7 +153,7 @@ namespace API.Parser
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Batman & Wildcat (1 of 3) // Batman & Wildcat (1 of 3)
new Regex( new Regex(
@"(?<Series>.*(\d{4})?)( |_)(?:\(\d+ of \d+)", @"(?<Series>.*(\d{4})?)( |_)(?:\((?<Volume>\d+) of \d+)",
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus) // Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)
new Regex( new Regex(
@ -171,11 +201,11 @@ namespace API.Parser
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Scott Pilgrim 02 - Scott Pilgrim vs. The World (2005) // Scott Pilgrim 02 - Scott Pilgrim vs. The World (2005)
new Regex( new Regex(
@"^(?<Series>.*)(?: |_)(?<Volume>\d+)", @"^(?<Series>.*)(?: |_)(?<!of )(?<Volume>\d+)",
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Batman & Catwoman - Trail of the Gun 01, Batman & Grendel (1996) 01 - Devil's Bones, Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus) // Batman & Catwoman - Trail of the Gun 01, Batman & Grendel (1996) 01 - Devil's Bones, Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus)
new Regex( new Regex(
@"^(?<Series>.*)(?: (?<Volume>\d+))", @"^(?<Series>.*)(?<!of)(?: (?<Volume>\d+))",
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Batman & Robin the Teen Wonder #0 // Batman & Robin the Teen Wonder #0
new Regex( new Regex(
@ -231,11 +261,14 @@ namespace API.Parser
new Regex( new Regex(
@"v\d+\.(?<Chapter>\d+(?:.\d+|-\d+)?)", @"v\d+\.(?<Chapter>\d+(?:.\d+|-\d+)?)",
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Mob Psycho 100 // Umineko no Naku Koro ni - Episode 3 - Banquet of the Golden Witch #02.cbz (Rare case, if causes issue remove)
new Regex(
@"^(?<Series>.*)(?: |_)#(?<Chapter>\d+)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz, Hinowa ga CRUSH! 018.5 (2019) (Digital) (LuCaZ).cbz // Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz, Hinowa ga CRUSH! 018.5 (2019) (Digital) (LuCaZ).cbz
new Regex( new Regex(
@"^(?!Vol)(?<Series>.*) (?<!vol\. )(?<Chapter>\d+(?:.\d+|-\d+)?)(?: \(\d{4}\))?", @"^(?!Vol)(?<Series>.*) (?<!vol\. )(?<Chapter>\d+(?:.\d+|-\d+)?)(?: \(\d{4}\))?(\b|_|-)",
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
// Tower Of God S01 014 (CBT) (digital).cbz // Tower Of God S01 014 (CBT) (digital).cbz
new Regex( new Regex(
@ -249,16 +282,28 @@ namespace API.Parser
new Regex( new Regex(
@"Chapter(?<Chapter>\d+(-\d+)?)", //(?:.\d+|-\d+)? @"Chapter(?<Chapter>\d+(-\d+)?)", //(?:.\d+|-\d+)?
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
}; };
private static readonly Regex[] MangaEditionRegex = { private static readonly Regex[] MangaEditionRegex = {
//Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz // Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz
new Regex( new Regex(
@"(?<Edition>({|\(|\[).* Edition(}|\)|\]))", @"(?<Edition>({|\(|\[).* Edition(}|\)|\]))",
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz // Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz
new Regex( new Regex(
@"(\b|_)(?<Edition>Omnibus)(\b|_)", @"(\b|_)(?<Edition>Omnibus(( |_)?Edition)?)(\b|_)?",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// To Love Ru v01 Uncensored (Ch.001-007)
new Regex(
@"(\b|_)(?<Edition>Uncensored)(\b|_)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// [dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz
new Regex(
@"(\b|_)(?<Edition>Digital(?: |_)Colored(?: |_)Comics)(\b|_)?",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
// AKIRA - c003 (v01) [Full Color] [Darkhorse].cbz
new Regex(
@"(\b|_)(?<Edition>Full(?: |_)Color)(\b|_)?",
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
}; };
@ -278,6 +323,14 @@ namespace API.Parser
RegexOptions.IgnoreCase | RegexOptions.Compiled), RegexOptions.IgnoreCase | RegexOptions.Compiled),
}; };
private static readonly Regex[] MangaSpecialRegex =
{
// All Keywords, does not account for checking if contains volume/chapter identification. Parser.Parse() will handle.
new Regex(
@"(?<Special>Specials?|OneShot|One\-Shot|Omake|Extra( Chapter)?|Art Collection)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
};
/// <summary> /// <summary>
/// Parses information out of a file path. Will fallback to using directory name if Series couldn't be parsed /// Parses information out of a file path. Will fallback to using directory name if Series couldn't be parsed
@ -315,6 +368,13 @@ namespace API.Parser
ret.Series = CleanTitle(ret.Series.Replace(edition, "")); ret.Series = CleanTitle(ret.Series.Replace(edition, ""));
ret.Edition = edition; ret.Edition = edition;
} }
var isSpecial = ParseMangaSpecial(fileName);
if (ret.Chapters == "0" && ret.Volumes == "0" && !string.IsNullOrEmpty(isSpecial))
{
ret.IsSpecial = true;
}
return ret.Series == string.Empty ? null : ret; return ret.Series == string.Empty ? null : ret;
@ -347,6 +407,23 @@ namespace API.Parser
return string.Empty; return string.Empty;
} }
public static string ParseMangaSpecial(string filePath)
{
foreach (var regex in MangaSpecialRegex)
{
var matches = regex.Matches(filePath);
foreach (Match match in matches)
{
if (match.Groups["Special"].Success && match.Groups["Special"].Value != string.Empty)
{
return match.Groups["Special"].Value;
}
}
}
return string.Empty;
}
public static string ParseSeries(string filename) public static string ParseSeries(string filename)
{ {
foreach (var regex in MangaSeriesRegex) foreach (var regex in MangaSeriesRegex)
@ -387,7 +464,7 @@ namespace API.Parser
var matches = regex.Matches(filename); var matches = regex.Matches(filename);
foreach (Match match in matches) foreach (Match match in matches)
{ {
if (match.Groups["Volume"] == Match.Empty) continue; if (!match.Groups["Volume"].Success || match.Groups["Volume"] == Match.Empty) continue;
var value = match.Groups["Volume"].Value; var value = match.Groups["Volume"].Value;
if (!value.Contains("-")) return RemoveLeadingZeroes(match.Groups["Volume"].Value); if (!value.Contains("-")) return RemoveLeadingZeroes(match.Groups["Volume"].Value);
@ -409,7 +486,7 @@ namespace API.Parser
var matches = regex.Matches(filename); var matches = regex.Matches(filename);
foreach (Match match in matches) foreach (Match match in matches)
{ {
if (match.Groups["Volume"] == Match.Empty) continue; if (!match.Groups["Volume"].Success || match.Groups["Volume"] == Match.Empty) continue;
var value = match.Groups["Volume"].Value; var value = match.Groups["Volume"].Value;
if (!value.Contains("-")) return RemoveLeadingZeroes(match.Groups["Volume"].Value); if (!value.Contains("-")) return RemoveLeadingZeroes(match.Groups["Volume"].Value);
@ -431,20 +508,16 @@ namespace API.Parser
var matches = regex.Matches(filename); var matches = regex.Matches(filename);
foreach (Match match in matches) foreach (Match match in matches)
{ {
if (match.Groups["Chapter"] != Match.Empty) if (!match.Groups["Chapter"].Success || match.Groups["Chapter"] == Match.Empty) continue;
{
var value = match.Groups["Chapter"].Value; var value = match.Groups["Chapter"].Value;
if (value.Contains("-")) if (!value.Contains("-")) return RemoveLeadingZeroes(match.Groups["Chapter"].Value);
{
var tokens = value.Split("-"); var tokens = value.Split("-");
var from = RemoveLeadingZeroes(tokens[0]); var from = RemoveLeadingZeroes(tokens[0]);
var to = RemoveLeadingZeroes(tokens[1]); var to = RemoveLeadingZeroes(tokens[1]);
return $"{from}-{to}"; return $"{@from}-{to}";
}
return RemoveLeadingZeroes(match.Groups["Chapter"].Value);
}
} }
} }
@ -459,7 +532,7 @@ namespace API.Parser
var matches = regex.Matches(filename); var matches = regex.Matches(filename);
foreach (Match match in matches) foreach (Match match in matches)
{ {
if (match.Groups["Chapter"] != Match.Empty) if (match.Groups["Chapter"].Success && match.Groups["Chapter"] != Match.Empty)
{ {
var value = match.Groups["Chapter"].Value; var value = match.Groups["Chapter"].Value;
@ -493,10 +566,41 @@ namespace API.Parser
} }
} }
} }
foreach (var regex in MangaEditionRegex)
{
var matches = regex.Matches(title);
foreach (Match match in matches)
{
if (match.Success)
{
title = title.Replace(match.Value, "");
}
}
}
return title; return title;
} }
private static string RemoveSpecialTags(string title)
{
foreach (var regex in MangaSpecialRegex)
{
var matches = regex.Matches(title);
foreach (Match match in matches)
{
if (match.Success)
{
title = title.Replace(match.Value, "");
}
}
}
return title;
}
/// <summary> /// <summary>
/// Translates _ -> spaces, trims front and back of string, removes release groups /// Translates _ -> spaces, trims front and back of string, removes release groups
/// </summary> /// </summary>
@ -508,6 +612,8 @@ namespace API.Parser
title = RemoveEditionTagHolders(title); title = RemoveEditionTagHolders(title);
title = RemoveSpecialTags(title);
title = title.Replace("_", " ").Trim(); title = title.Replace("_", " ").Trim();
if (title.EndsWith("-")) if (title.EndsWith("-"))
{ {

View File

@ -24,5 +24,10 @@ namespace API.Parser
/// This can potentially story things like "Omnibus, Color, Full Contact Edition, Extra, Final, etc" /// This can potentially story things like "Omnibus, Color, Full Contact Edition, Extra, Final, etc"
/// </summary> /// </summary>
public string Edition { get; set; } = ""; public string Edition { get; set; } = "";
/// <summary>
/// If the file contains no volume/chapter information and contains Special Keywords <see cref="Parser.MangaSpecialRegex"/>
/// </summary>
public bool IsSpecial { get; set; } = false;
} }
} }

View File

@ -49,15 +49,15 @@ namespace API.Services.Tasks
{ {
// NOTE: This solution isn't the best, but it has potential. We need to handle a few other cases so it works great. // NOTE: This solution isn't the best, but it has potential. We need to handle a few other cases so it works great.
return false; return false;
// if (/*_environment.IsProduction() && */!_forceUpdate && Directory.GetLastWriteTime(folder.Path) < folder.LastScanned) // if (!_forceUpdate && Directory.GetLastWriteTime(folder.Path) < folder.LastScanned)
// { // {
// _logger.LogDebug($"{folder.Path} hasn't been updated since last scan. Skipping."); // _logger.LogDebug("{FolderPath} hasn't been modified since last scan. Skipping", folder.Path);
// skippedFolders += 1; // skippedFolders += 1;
// return true; // return true;
// } // }
//
// return false; //return false;
} }
private void Cleanup() private void Cleanup()
@ -134,7 +134,6 @@ namespace API.Services.Tasks
if (Task.Run(() => _unitOfWork.Complete()).Result) if (Task.Run(() => _unitOfWork.Complete()).Result)
{ {
_logger.LogInformation("Scan completed on {LibraryName}. Parsed {ParsedSeriesCount} series in {ElapsedScanTime} ms", library.Name, series.Keys.Count, sw.ElapsedMilliseconds); _logger.LogInformation("Scan completed on {LibraryName}. Parsed {ParsedSeriesCount} series in {ElapsedScanTime} ms", library.Name, series.Keys.Count, sw.ElapsedMilliseconds);
} }
else else
@ -184,7 +183,7 @@ namespace API.Services.Tasks
existingSeries.NormalizedName = Parser.Parser.Normalize(key); existingSeries.NormalizedName = Parser.Parser.Normalize(key);
existingSeries.LocalizedName ??= key; existingSeries.LocalizedName ??= key;
} }
// Now, we only have to deal with series that exist on disk. Let's recalculate the volumes for each series // Now, we only have to deal with series that exist on disk. Let's recalculate the volumes for each series
var librarySeries = library.Series.ToList(); var librarySeries = library.Series.ToList();
Parallel.ForEach(librarySeries, (series) => Parallel.ForEach(librarySeries, (series) =>
@ -222,7 +221,7 @@ namespace API.Services.Tasks
series.Volumes.Add(volume); series.Volumes.Add(volume);
} }
volume.IsSpecial = volume.Number == 0 && infos.All(p => p.Chapters == "0"); volume.IsSpecial = volume.Number == 0 && infos.All(p => p.Chapters == "0" || p.IsSpecial);
_logger.LogDebug("Parsing {SeriesName} - Volume {VolumeNumber}", series.Name, volume.Name); _logger.LogDebug("Parsing {SeriesName} - Volume {VolumeNumber}", series.Name, volume.Name);
UpdateChapters(volume, infos); UpdateChapters(volume, infos);
volume.Pages = volume.Chapters.Sum(c => c.Pages); volume.Pages = volume.Chapters.Sum(c => c.Pages);
@ -315,6 +314,24 @@ namespace API.Services.Tasks
{ {
if (info.Series == string.Empty) return; if (info.Series == string.Empty) return;
// Check if normalized info.Series already exists and if so, update info to use that name instead
var normalizedSeries = Parser.Parser.Normalize(info.Series);
var existingName = _scannedSeries.SingleOrDefault(p => Parser.Parser.Normalize(p.Key) == normalizedSeries)
.Key;
if (!string.IsNullOrEmpty(existingName))
{
_logger.LogInformation("Found duplicate parsed infos, merged {Original} into {Merged}", info.Series, existingName);
info.Series = existingName;
}
// TODO: For all parsedSeries, any infos that contain same series name and IsSpecial is true are combined
// foreach (var series in parsedSeries)
// {
// var seriesName = series.Key;
// if (parsedSeries.ContainsKey(seriesName))
// }
_scannedSeries.AddOrUpdate(info.Series, new List<ParserInfo>() {info}, (_, oldValue) => _scannedSeries.AddOrUpdate(info.Series, new List<ParserInfo>() {info}, (_, oldValue) =>
{ {
oldValue ??= new List<ParserInfo>(); oldValue ??= new List<ParserInfo>();

View File

@ -136,7 +136,7 @@ namespace API
applicationLifetime.ApplicationStopping.Register(OnShutdown); applicationLifetime.ApplicationStopping.Register(OnShutdown);
applicationLifetime.ApplicationStarted.Register(() => applicationLifetime.ApplicationStarted.Register(() =>
{ {
Console.WriteLine("Kavita - v0.3.5"); Console.WriteLine("Kavita - v0.3.6");
}); });
} }

View File

@ -65,12 +65,15 @@ Package()
# TODO: Use no-restore? Because Build should have already done it for us # TODO: Use no-restore? Because Build should have already done it for us
echo "Building" echo "Building"
cd API cd API
echo dotnet publish -c release --self-contained --runtime $runtime -o "$lOutputFolder" --framework $framework echo dotnet publish -c Release --self-contained --runtime $runtime -o "$lOutputFolder" --framework $framework
dotnet publish -c release --self-contained --runtime $runtime -o "$lOutputFolder" --framework $framework dotnet publish -c Release --self-contained --runtime $runtime -o "$lOutputFolder" --framework $framework
echo "Copying Install information" echo "Copying Install information"
cp ../INSTALL.txt "$lOutputFolder"/README.txt cp ../INSTALL.txt "$lOutputFolder"/README.txt
echo "Copying LICENSE"
cp ../LICENSE "$lOutputFolder"/LICENSE.txt
echo "Renaming API -> Kavita" echo "Renaming API -> Kavita"
mv "$lOutputFolder"/API "$lOutputFolder"/Kavita mv "$lOutputFolder"/API "$lOutputFolder"/Kavita