Comic enhancements (#645)

* Adding multiple cases for comic naming conventions

* Changing "Chapter" to "Issue" for comic libraries

* Fixed an issue where the Parse method was using filename with extension to run regex matching, while it should be running on name without extension.

* Refactored to use Getter

* Cleaned up file to use conditional labelling rather than conditional html fragments

* Refactored code to properly check against library type for a given readinglist item

* Cleaned up series detail

* Conditionally remove special tags during parse

* Setup ParseInfoTests for ComicParserTests and also added unit tests from other comic issues created.

* Added more regex cases for naming patterns reported to be common with comics. Some cases added without regex.

* Pushing up changes

Fixed issue with cleanTitleTest.
Tried some patterns for "Cyberpunk 2077" but reverted

* Updated some cases and some spacing on Parser. Cyberpunk 2077 is not implemented as long as there is a # before issue number.

* Fixed the case for Special parsing on TPB. Fixed a piece of code that got deleted that prevented specials from rendering on volumes tab.

* Potential fix for parsing Cyberpunk 2077

- Added a ComicsSeriesSpecialCasesRegex and passed any filename that contains "Cyberpunk 2077" over to it so we can parse it separately. This could be used for any other potential problem series.

* Revert "Potential fix for parsing Cyberpunk 2077"

This reverts commit a14417e640ddb7ab27f66bcc27ff5ecc41581b25.

* Added more tests

* Refactored all places in Kavita to use Book, Issue, or Chapter depending on the Library type. Updated Volumes/Chapters to remove Volumes to make it cleaner.

* Removed some leftover test code

Co-authored-by: Joseph Milazzo <joseph.v.milazzo@gmail.com>
This commit is contained in:
Robbie Davis 2021-10-07 10:49:13 -04:00 committed by GitHub
parent f5136c8127
commit 3293e5b424
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 392 additions and 265 deletions

View File

@ -1,9 +1,21 @@
using Xunit; using System;
using System.Collections.Generic;
using API.Entities.Enums;
using API.Parser;
using Xunit;
using Xunit.Abstractions;
namespace API.Tests.Parser namespace API.Tests.Parser
{ {
public class ComicParserTests public class ComicParserTests
{ {
private readonly ITestOutputHelper _testOutputHelper;
public ComicParserTests(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
}
[Theory] [Theory]
[InlineData("01 Spider-Man & Wolverine 01.cbr", "Spider-Man & Wolverine")] [InlineData("01 Spider-Man & Wolverine 01.cbr", "Spider-Man & Wolverine")]
[InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", "Asterix the Gladiator")] [InlineData("04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)", "Asterix the Gladiator")]
@ -29,7 +41,21 @@ namespace API.Tests.Parser
[InlineData("Batman Wayne Family Adventures - Ep. 001 - Moving In", "Batman Wayne Family Adventures")] [InlineData("Batman Wayne Family Adventures - Ep. 001 - Moving In", "Batman Wayne Family Adventures")]
[InlineData("Saga 001 (2012) (Digital) (Empire-Zone).cbr", "Saga")] [InlineData("Saga 001 (2012) (Digital) (Empire-Zone).cbr", "Saga")]
[InlineData("spawn-123", "spawn")] [InlineData("spawn-123", "spawn")]
[InlineData("Spawn 062 (1997) (digital) (TLK-EMPIRE-HD).cbr", "Spawn")]
[InlineData("Batman Beyond 04 (of 6) (1999)", "Batman Beyond")] [InlineData("Batman Beyond 04 (of 6) (1999)", "Batman Beyond")]
[InlineData("Batman Beyond 001 (2012)", "Batman Beyond")]
[InlineData("Batman Beyond 2.0 001 (2013)", "Batman Beyond 2.0")]
[InlineData("Batman - Catwoman 001 (2021) (Webrip) (The Last Kryptonian-DCP)", "Batman - Catwoman")]
[InlineData("Chew v1 - Taster´s Choise (2012) (Digital) (1920) (Kingpin-Empire)", "Chew")]
[InlineData("Chew Script Book (2011) (digital-Empire) SP04", "Chew Script Book")]
[InlineData("Batman - Detective Comics - Rebirth Deluxe Edition Book 02 (2018) (digital) (Son of Ultron-Empire)", "Batman - Detective Comics - Rebirth Deluxe Edition Book")]
[InlineData("Cyberpunk 2077 - Your Voice #01", "Cyberpunk 2077 - Your Voice")]
[InlineData("Cyberpunk 2077 #01", "Cyberpunk 2077")]
[InlineData("Cyberpunk 2077 - Trauma Team #04.cbz", "Cyberpunk 2077 - Trauma Team")]
[InlineData("Batgirl Vol.2000 #57 (December, 2004)", "Batgirl")]
[InlineData("Batgirl V2000 #57", "Batgirl")]
[InlineData("Fables 021 (2004) (Digital) (Nahga-Empire).cbr", "Fables")]
public void ParseComicSeriesTest(string filename, string expected) public void ParseComicSeriesTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Parser.Parser.ParseComicSeries(filename)); Assert.Equal(expected, API.Parser.Parser.ParseComicSeries(filename));
@ -54,6 +80,17 @@ namespace API.Tests.Parser
[InlineData("Invincible 033.5 - Marvel Team-Up 14 (2006) (digital) (Minutemen-Slayer)", "0")] [InlineData("Invincible 033.5 - Marvel Team-Up 14 (2006) (digital) (Minutemen-Slayer)", "0")]
[InlineData("Cyberpunk 2077 - Trauma Team 04.cbz", "0")] [InlineData("Cyberpunk 2077 - Trauma Team 04.cbz", "0")]
[InlineData("spawn-123", "0")] [InlineData("spawn-123", "0")]
[InlineData("Spawn 062 (1997) (digital) (TLK-EMPIRE-HD).cbr", "0")]
[InlineData("Batman Beyond 04 (of 6) (1999)", "0")]
[InlineData("Batman Beyond 001 (2012)", "0")]
[InlineData("Batman Beyond 2.0 001 (2013)", "0")]
[InlineData("Batman - Catwoman 001 (2021) (Webrip) (The Last Kryptonian-DCP)", "0")]
[InlineData("Chew v1 - Taster´s Choise (2012) (Digital) (1920) (Kingpin-Empire)", "1")]
[InlineData("Chew Script Book (2011) (digital-Empire) SP04", "0")]
[InlineData("Batgirl Vol.2000 #57 (December, 2004)", "2000")]
[InlineData("Batgirl V2000 #57", "2000")]
[InlineData("Fables 021 (2004) (Digital) (Nahga-Empire).cbr", "0")]
[InlineData("Cyberpunk 2077 - Trauma Team 04.cbz", "0")]
public void ParseComicVolumeTest(string filename, string expected) public void ParseComicVolumeTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Parser.Parser.ParseComicVolume(filename)); Assert.Equal(expected, API.Parser.Parser.ParseComicVolume(filename));
@ -80,12 +117,75 @@ namespace API.Tests.Parser
[InlineData("Batman Wayne Family Adventures - Ep. 014 - Moving In", "14")] [InlineData("Batman Wayne Family Adventures - Ep. 014 - Moving In", "14")]
[InlineData("Saga 001 (2012) (Digital) (Empire-Zone)", "1")] [InlineData("Saga 001 (2012) (Digital) (Empire-Zone)", "1")]
[InlineData("spawn-123", "123")] [InlineData("spawn-123", "123")]
[InlineData("Spawn 062 (1997) (digital) (TLK-EMPIRE-HD).cbr", "62")]
[InlineData("Batman Beyond 04 (of 6) (1999)", "4")] [InlineData("Batman Beyond 04 (of 6) (1999)", "4")]
[InlineData("Invincible 052 (c2c) (2008) (Minutemen-TheCouple)", "52")] [InlineData("Invincible 052 (c2c) (2008) (Minutemen-TheCouple)", "52")]
[InlineData("Y - The Last Man #001", "1")] [InlineData("Y - The Last Man #001", "1")]
[InlineData("Batman Beyond 001 (2012)", "1")]
[InlineData("Batman Beyond 2.0 001 (2013)", "1")]
[InlineData("Batman - Catwoman 001 (2021) (Webrip) (The Last Kryptonian-DCP)", "1")]
[InlineData("Chew v1 - Taster´s Choise (2012) (Digital) (1920) (Kingpin-Empire)", "0")]
[InlineData("Chew Script Book (2011) (digital-Empire) SP04", "0")]
[InlineData("Batgirl Vol.2000 #57 (December, 2004)", "57")]
[InlineData("Batgirl V2000 #57", "57")]
[InlineData("Fables 021 (2004) (Digital) (Nahga-Empire).cbr", "21")]
[InlineData("Cyberpunk 2077 - Trauma Team #04.cbz", "4")]
public void ParseComicChapterTest(string filename, string expected) public void ParseComicChapterTest(string filename, string expected)
{ {
Assert.Equal(expected, API.Parser.Parser.ParseComicChapter(filename)); Assert.Equal(expected, API.Parser.Parser.ParseComicChapter(filename));
} }
[Theory]
[InlineData("Batman - Detective Comics - Rebirth Deluxe Edition Book 02 (2018) (digital) (Son of Ultron-Empire)", true)]
[InlineData("Zombie Tramp vs. Vampblade TPB (2016) (Digital) (TheArchivist-Empire)", true)]
[InlineData("Baldwin the Brave & Other Tales Special SP1.cbr", true)]
[InlineData("Mouse Guard Specials - Spring 1153 - Fraggle Rock FCBD 2010", true)]
public void ParseComicSpecialTest(string input, bool expected)
{
Assert.Equal(expected, !string.IsNullOrEmpty(API.Parser.Parser.ParseComicSpecial(input)));
}
[Fact]
public void ParseInfoTest()
{
const string rootPath = @"E:/Comics/";
var expected = new Dictionary<string, ParserInfo>();
var filepath = @"E:/Comics/Teen Titans/Teen Titans v1 Annual 01 (1967) SP01.cbr";
expected.Add(filepath, new ParserInfo
{
Series = "Teen Titans", Volumes = "0",
Chapters = "0", Filename = "Teen Titans v1 Annual 01 (1967) SP01.cbr", Format = MangaFormat.Archive,
FullFilePath = filepath
});
foreach (var file in expected.Keys)
{
var expectedInfo = expected[file];
var actual = API.Parser.Parser.Parse(file, rootPath);
if (expectedInfo == null)
{
Assert.Null(actual);
return;
}
Assert.NotNull(actual);
_testOutputHelper.WriteLine($"Validating {file}");
Assert.Equal(expectedInfo.Format, actual.Format);
_testOutputHelper.WriteLine("Format ✓");
Assert.Equal(expectedInfo.Series, actual.Series);
_testOutputHelper.WriteLine("Series ✓");
Assert.Equal(expectedInfo.Chapters, actual.Chapters);
_testOutputHelper.WriteLine("Chapters ✓");
Assert.Equal(expectedInfo.Volumes, actual.Volumes);
_testOutputHelper.WriteLine("Volumes ✓");
Assert.Equal(expectedInfo.Edition, actual.Edition);
_testOutputHelper.WriteLine("Edition ✓");
Assert.Equal(expectedInfo.Filename, actual.Filename);
_testOutputHelper.WriteLine("Filename ✓");
Assert.Equal(expectedInfo.FullFilePath, actual.FullFilePath);
_testOutputHelper.WriteLine("FullFilePath ✓");
}
}
} }
} }

View File

@ -11,6 +11,7 @@ namespace API.Tests.Parser
[InlineData("Beastars SP01", true)] [InlineData("Beastars SP01", true)]
[InlineData("Beastars Special 01", false)] [InlineData("Beastars Special 01", false)]
[InlineData("Beastars Extra 01", false)] [InlineData("Beastars Extra 01", false)]
[InlineData("Batman Beyond - Return of the Joker (2001) SP01", true)]
public void HasSpecialTest(string input, bool expected) public void HasSpecialTest(string input, bool expected)
{ {
Assert.Equal(expected, HasSpecialMarker(input)); Assert.Equal(expected, HasSpecialMarker(input));
@ -35,14 +36,15 @@ namespace API.Tests.Parser
} }
[Theory] [Theory]
[InlineData("Hello_I_am_here", "Hello I am here")] [InlineData("Hello_I_am_here", false, "Hello I am here")]
[InlineData("Hello_I_am_here ", "Hello I am here")] [InlineData("Hello_I_am_here ", false, "Hello I am here")]
[InlineData("[ReleaseGroup] The Title", "The Title")] [InlineData("[ReleaseGroup] The Title", false, "The Title")]
[InlineData("[ReleaseGroup]_The_Title", "The Title")] [InlineData("[ReleaseGroup]_The_Title", false, "The Title")]
[InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1", "Kasumi Otoko no Ko v1.1")] [InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1", false, "Kasumi Otoko no Ko v1.1")]
public void CleanTitleTest(string input, string expected) [InlineData("Batman - Detective Comics - Rebirth Deluxe Edition Book 04 (2019) (digital) (Son of Ultron-Empire)", true, "Batman - Detective Comics - Rebirth Deluxe Edition")]
public void CleanTitleTest(string input, bool isComic, string expected)
{ {
Assert.Equal(expected, CleanTitle(input)); Assert.Equal(expected, CleanTitle(input, isComic));
} }

View File

@ -25,32 +25,24 @@ namespace API.Parser
RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant; RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant;
public static readonly Regex FontSrcUrlRegex = new Regex(@"(src:url\(.{1})" + "([^\"']*)" + @"(.{1}\))", public static readonly Regex FontSrcUrlRegex = new Regex(@"(src:url\(.{1})" + "([^\"']*)" + @"(.{1}\))",
MatchOptions, MatchOptions, RegexTimeout);
RegexTimeout);
public static readonly Regex CssImportUrlRegex = new Regex("(@import\\s[\"|'])(?<Filename>[\\w\\d/\\._-]+)([\"|'];?)", public static readonly Regex CssImportUrlRegex = new Regex("(@import\\s[\"|'])(?<Filename>[\\w\\d/\\._-]+)([\"|'];?)",
MatchOptions, MatchOptions, RegexTimeout);
RegexTimeout);
private static readonly string XmlRegexExtensions = @"\.xml"; private static readonly string XmlRegexExtensions = @"\.xml";
private static readonly Regex ImageRegex = new Regex(ImageFileExtensions, private static readonly Regex ImageRegex = new Regex(ImageFileExtensions,
MatchOptions, MatchOptions, RegexTimeout);
RegexTimeout);
private static readonly Regex ArchiveFileRegex = new Regex(ArchiveFileExtensions, private static readonly Regex ArchiveFileRegex = new Regex(ArchiveFileExtensions,
MatchOptions, MatchOptions, RegexTimeout);
RegexTimeout);
private static readonly Regex XmlRegex = new Regex(XmlRegexExtensions, private static readonly Regex XmlRegex = new Regex(XmlRegexExtensions,
MatchOptions, MatchOptions, RegexTimeout);
RegexTimeout);
private static readonly Regex BookFileRegex = new Regex(BookFileExtensions, private static readonly Regex BookFileRegex = new Regex(BookFileExtensions,
MatchOptions, MatchOptions, RegexTimeout);
RegexTimeout);
private static readonly Regex CoverImageRegex = new Regex(@"(?<![[a-z]\d])(?:!?)(cover|folder)(?![\w\d])", private static readonly Regex CoverImageRegex = new Regex(@"(?<![[a-z]\d])(?:!?)(cover|folder)(?![\w\d])",
MatchOptions, MatchOptions, RegexTimeout);
RegexTimeout);
private static readonly Regex NormalizeRegex = new Regex(@"[^a-zA-Z0-9\+]", private static readonly Regex NormalizeRegex = new Regex(@"[^a-zA-Z0-9\+]",
MatchOptions, MatchOptions, RegexTimeout);
RegexTimeout);
private static readonly Regex[] MangaVolumeRegex = new[] private static readonly Regex[] MangaVolumeRegex = new[]
@ -58,43 +50,35 @@ namespace API.Parser
// Dance in the Vampire Bund v16-17 // Dance in the Vampire Bund v16-17
new Regex( new Regex(
@"(?<Series>.*)(\b|_)v(?<Volume>\d+-?\d+)( |_)", @"(?<Series>.*)(\b|_)v(?<Volume>\d+-?\d+)( |_)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// NEEDLESS_Vol.4_-Simeon_6_v2[SugoiSugoi].rar // NEEDLESS_Vol.4_-Simeon_6_v2[SugoiSugoi].rar
new Regex( new Regex(
@"(?<Series>.*)(\b|_)(?!\[)(vol\.?)(?<Volume>\d+(-\d+)?)(?!\])", @"(?<Series>.*)(\b|_)(?!\[)(vol\.?)(?<Volume>\d+(-\d+)?)(?!\])",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Historys Strongest Disciple Kenichi_v11_c90-98.zip or Dance in the Vampire Bund v16-17 // Historys Strongest Disciple Kenichi_v11_c90-98.zip or Dance in the Vampire Bund v16-17
new Regex( new Regex(
@"(?<Series>.*)(\b|_)(?!\[)v(?<Volume>\d+(-\d+)?)(?!\])", @"(?<Series>.*)(\b|_)(?!\[)v(?<Volume>\d+(-\d+)?)(?!\])",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Kodomo no Jikan vol. 10, [dmntsf.net] One Piece - Digital Colored Comics Vol. 20.5-21.5 Ch. 177 // Kodomo no Jikan vol. 10, [dmntsf.net] One Piece - Digital Colored Comics Vol. 20.5-21.5 Ch. 177
new Regex( new Regex(
@"(?<Series>.*)(\b|_)(vol\.? ?)(?<Volume>\d+(\.\d)?(-\d+)?(\.\d)?)", @"(?<Series>.*)(\b|_)(vol\.? ?)(?<Volume>\d+(\.\d)?(-\d+)?(\.\d)?)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb) // Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)
new Regex( new Regex(
@"(vol\.? ?)(?<Volume>\d+(\.\d)?)", @"(vol\.? ?)(?<Volume>\d+(\.\d)?)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Tonikaku Cawaii [Volume 11].cbz // Tonikaku Cawaii [Volume 11].cbz
new Regex( new Regex(
@"(volume )(?<Volume>\d+(\.\d)?)", @"(volume )(?<Volume>\d+(\.\d)?)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// 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+))",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// vol_001-1.cbz for MangaPy default naming convention // vol_001-1.cbz for MangaPy default naming convention
new Regex( new Regex(
@"(vol_)(?<Volume>\d+(\.\d)?)", @"(vol_)(?<Volume>\d+(\.\d)?)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
}; };
private static readonly Regex[] MangaSeriesRegex = new[] private static readonly Regex[] MangaSeriesRegex = new[]
@ -102,13 +86,11 @@ namespace API.Parser
// Grand Blue Dreaming - SP02 // Grand Blue Dreaming - SP02
new Regex( new Regex(
@"(?<Series>.*)(\b|_|-|\s)(?:sp)\d", @"(?<Series>.*)(\b|_|-|\s)(?:sp)\d",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// [SugoiSugoi]_NEEDLESS_Vol.2_-_Disk_The_Informant_5_[ENG].rar, Yuusha Ga Shinda! - Vol.tbd Chapter 27.001 V2 Infection ①.cbz // [SugoiSugoi]_NEEDLESS_Vol.2_-_Disk_The_Informant_5_[ENG].rar, Yuusha Ga Shinda! - Vol.tbd Chapter 27.001 V2 Infection ①.cbz
new Regex( new Regex(
@"^(?<Series>.*)( |_)Vol\.?(\d+|tbd)", @"^(?<Series>.*)( |_)Vol\.?(\d+|tbd)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Mad Chimera World - Volume 005 - Chapter 026.cbz (couldn't figure out how to get Volume negative lookaround working on below regex), // Mad Chimera World - Volume 005 - Chapter 026.cbz (couldn't figure out how to get Volume negative lookaround working on below regex),
// The Duke of Death and His Black Maid - Vol. 04 Ch. 054.5 - V4 Omake // The Duke of Death and His Black Maid - Vol. 04 Ch. 054.5 - V4 Omake
new Regex( new Regex(
@ -123,23 +105,19 @@ namespace API.Parser
// Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA], Black Bullet - v4 c17 [batoto] // Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA], Black Bullet - v4 c17 [batoto]
new Regex( new Regex(
@"(?<Series>.*)( - )(?:v|vo|c)\d", @"(?<Series>.*)( - )(?:v|vo|c)\d",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Kedouin Makoto - Corpse Party Musume, Chapter 19 [Dametrans].zip // Kedouin Makoto - Corpse Party Musume, Chapter 19 [Dametrans].zip
new Regex( new Regex(
@"(?<Series>.*)(?:, Chapter )(?<Chapter>\d+)", @"(?<Series>.*)(?:, Chapter )(?<Chapter>\d+)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Please Go Home, Akutsu-San! - Chapter 038.5 - Volume Announcement.cbz // Please Go Home, Akutsu-San! - Chapter 038.5 - Volume Announcement.cbz
new Regex( new Regex(
@"(?<Series>.*)(\s|_|-)(?!Vol)(\s|_|-)(?:Chapter)(\s|_|-)(?<Chapter>\d+)", @"(?<Series>.*)(\s|_|-)(?!Vol)(\s|_|-)(?:Chapter)(\s|_|-)(?<Chapter>\d+)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// [dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz // [dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz
new Regex( new Regex(
@"(?<Series>.*) (\b|_|-)(vol)\.?(\s|-|_)?\d+", @"(?<Series>.*) (\b|_|-)(vol)\.?(\s|-|_)?\d+",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// [xPearse] Kyochuu Rettou Volume 1 [English] [Manga] [Volume Scans] // [xPearse] Kyochuu Rettou Volume 1 [English] [Manga] [Volume Scans]
new Regex( new Regex(
@"(?<Series>.*) (\b|_|-)(vol)(ume)", @"(?<Series>.*) (\b|_|-)(vol)(ume)",
@ -148,121 +126,98 @@ namespace API.Parser
//Knights of Sidonia c000 (S2 LE BD Omake - BLAME!) [Habanero Scans] //Knights of Sidonia c000 (S2 LE BD Omake - BLAME!) [Habanero Scans]
new Regex( new Regex(
@"(?<Series>.*)(\bc\d+\b)", @"(?<Series>.*)(\bc\d+\b)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
//Tonikaku Cawaii [Volume 11], Darling in the FranXX - Volume 01.cbz //Tonikaku Cawaii [Volume 11], Darling in the FranXX - Volume 01.cbz
new Regex( new Regex(
@"(?<Series>.*)(?: _|-|\[|\()\s?vol(ume)?", @"(?<Series>.*)(?: _|-|\[|\()\s?vol(ume)?",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Momo The Blood Taker - Chapter 027 Violent Emotion.cbz, Grand Blue Dreaming - SP02 Extra (2019) (Digital) (danke-Empire).cbz // Momo The Blood Taker - Chapter 027 Violent Emotion.cbz, Grand Blue Dreaming - SP02 Extra (2019) (Digital) (danke-Empire).cbz
new Regex( new Regex(
@"^(?<Series>(?!Vol).+?)(?:(ch(apter|\.)(\b|_|-|\s))|sp)\d", @"^(?<Series>(?!Vol).+?)(?:(ch(apter|\.)(\b|_|-|\s))|sp)\d",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// 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|ch\.?|c)\d+", @"(?<Series>.*) (\b|_|-)(v|ch\.?|c)\d+",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
//Ichinensei_ni_Nacchattara_v01_ch01_[Taruby]_v1.1.zip must be before [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip //Ichinensei_ni_Nacchattara_v01_ch01_[Taruby]_v1.1.zip must be before [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip
// due to duplicate version identifiers in file. // due to duplicate version identifiers in file.
new Regex( new Regex(
@"(?<Series>.*)(v|s)\d+(-\d+)?(_|\s)", @"(?<Series>.*)(v|s)\d+(-\d+)?(_|\s)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
//[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip //[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip
new Regex( new Regex(
@"(?<Series>.*)(v|s)\d+(-\d+)?", @"(?<Series>.*)(v|s)\d+(-\d+)?",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz // Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz
new Regex( new Regex(
@"(?<Series>.*) (?<Chapter>\d+) (?:\(\d{4}\)) ", @"(?<Series>.*) (?<Chapter>\d+) (?:\(\d{4}\)) ",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Goblin Slayer - Brand New Day 006.5 (2019) (Digital) (danke-Empire) // Goblin Slayer - Brand New Day 006.5 (2019) (Digital) (danke-Empire)
new Regex( new Regex(
@"(?<Series>.*) (?<Chapter>\d+(?:.\d+|-\d+)?) \(\d{4}\)", @"(?<Series>.*) (?<Chapter>\d+(?:.\d+|-\d+)?) \(\d{4}\)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Noblesse - Episode 429 (74 Pages).7z // Noblesse - Episode 429 (74 Pages).7z
new Regex( new Regex(
@"(?<Series>.*)(\s|_)(?:Episode|Ep\.?)(\s|_)(?<Chapter>\d+(?:.\d+|-\d+)?)", @"(?<Series>.*)(\s|_)(?:Episode|Ep\.?)(\s|_)(?<Chapter>\d+(?:.\d+|-\d+)?)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Akame ga KILL! ZERO (2016-2019) (Digital) (LuCaZ) // Akame ga KILL! ZERO (2016-2019) (Digital) (LuCaZ)
new Regex( new Regex(
@"(?<Series>.*)\(\d", @"(?<Series>.*)\(\d",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Tonikaku Kawaii (Ch 59-67) (Ongoing) // Tonikaku Kawaii (Ch 59-67) (Ongoing)
new Regex( new Regex(
@"(?<Series>.*)(\s|_)\((c\s|ch\s|chapter\s)", @"(?<Series>.*)(\s|_)\((c\s|ch\s|chapter\s)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Black Bullet (This is very loose, keep towards bottom) // 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+",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// [Hidoi]_Amaenaideyo_MS_vol01_chp02.rar // [Hidoi]_Amaenaideyo_MS_vol01_chp02.rar
new Regex( new Regex(
@"(?<Series>.*)( |_)(vol\d+)?( |_)(?:Chp\.? ?\d+)", @"(?<Series>.*)( |_)(vol\d+)?( |_)(?:Chp\.? ?\d+)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Mahoutsukai to Deshi no Futekisetsu na Kankei Chp. 1 // Mahoutsukai to Deshi no Futekisetsu na Kankei Chp. 1
new Regex( new Regex(
@"(?<Series>.*)( |_)(?:Chp.? ?\d+)", @"(?<Series>.*)( |_)(?:Chp.? ?\d+)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Chapter 01 // Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Chapter 01
new Regex( new Regex(
@"^(?!Vol)(?<Series>.*)( |_)Chapter( |_)(\d+)", @"^(?!Vol)(?<Series>.*)( |_)Chapter( |_)(\d+)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Fullmetal Alchemist chapters 101-108.cbz // Fullmetal Alchemist chapters 101-108.cbz
new Regex( new Regex(
@"^(?!vol)(?<Series>.*)( |_)(chapters( |_)?)\d+-?\d*", @"^(?!vol)(?<Series>.*)( |_)(chapters( |_)?)\d+-?\d*",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Umineko no Naku Koro ni - Episode 1 - Legend of the Golden Witch #1 // Umineko no Naku Koro ni - Episode 1 - Legend of the Golden Witch #1
new Regex( new Regex(
@"^(?!Vol\.?)(?<Series>.*)( |_|-)(?<!-)(episode|chapter|(ch\.?) ?)\d+-?\d*", @"^(?!Vol\.?)(?<Series>.*)( |_|-)(?<!-)(episode|chapter|(ch\.?) ?)\d+-?\d*",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Baketeriya ch01-05.zip // Baketeriya ch01-05.zip
new Regex( new Regex(
@"^(?!Vol)(?<Series>.*)ch\d+-?\d?", @"^(?!Vol)(?<Series>.*)ch\d+-?\d?",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Magi - Ch.252-005.cbz // Magi - Ch.252-005.cbz
new Regex( new Regex(
@"(?<Series>.*)( ?- ?)Ch\.\d+-?\d*", @"(?<Series>.*)( ?- ?)Ch\.\d+-?\d*",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// [BAA]_Darker_than_Black_Omake-1.zip // [BAA]_Darker_than_Black_Omake-1.zip
new Regex( new Regex(
@"^(?!Vol)(?<Series>.*)(-)\d+-?\d*", // This catches a lot of stuff ^(?!Vol)(?<Series>.*)( |_)(\d+) @"^(?!Vol)(?<Series>.*)(-)\d+-?\d*", // This catches a lot of stuff ^(?!Vol)(?<Series>.*)( |_)(\d+)
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Kodoja #001 (March 2016) // Kodoja #001 (March 2016)
new Regex( new Regex(
@"(?<Series>.*)(\s|_|-)#", @"(?<Series>.*)(\s|_|-)#",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Baketeriya ch01-05.zip, Akiiro Bousou Biyori - 01.jpg, Beelzebub_172_RHS.zip, Cynthia the Mission 29.rar, A Compendium of Ghosts - 031 - The Third Story_ Part 12 (Digital) (Cobalt001) // Baketeriya ch01-05.zip, Akiiro Bousou Biyori - 01.jpg, Beelzebub_172_RHS.zip, Cynthia the Mission 29.rar, A Compendium of Ghosts - 031 - The Third Story_ Part 12 (Digital) (Cobalt001)
new Regex( new Regex(
@"^(?!Vol\.?)(?<Series>.+?)( |_|-)(?<!-)(ch)?\d+-?\d*", @"^(?!Vol\.?)(?<Series>.+?)( |_|-)(?<!-)(ch)?\d+-?\d*",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// [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(
@"^(?!Vol)(?<Series>.*)( |_|-)(ch?)\d+", @"^(?!Vol)(?<Series>.*)( |_|-)(ch?)\d+",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
}; };
private static readonly Regex[] ComicSeriesRegex = new[] private static readonly Regex[] ComicSeriesRegex = new[]
@ -270,115 +225,79 @@ namespace API.Parser
// Invincible Vol 01 Family matters (2005) (Digital) // Invincible Vol 01 Family matters (2005) (Digital)
new Regex( new Regex(
@"(?<Series>.*)(\b|_)(vol\.?)( |_)(?<Volume>\d+(-\d+)?)", @"(?<Series>.*)(\b|_)(vol\.?)( |_)(?<Volume>\d+(-\d+)?)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout), // Batman Beyond 2.0 001 (2013)
new Regex(
@"^(?<Series>.+?\S\.\d) (?<Chapter>\d+)",
MatchOptions, RegexTimeout),
// 04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS) // 04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)
new Regex( new Regex(
@"^(?<Volume>\d+) (- |_)?(?<Series>.*(\d{4})?)( |_)(\(|\d+)", @"^(?<Volume>\d+) (- |_)?(?<Series>.*(\d{4})?)( |_)(\(|\d+)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// 01 Spider-Man & Wolverine 01.cbr // 01 Spider-Man & Wolverine 01.cbr
new Regex( new Regex(
@"^(?<Volume>\d+) (?:- )?(?<Series>.*) (\d+)?", @"^(?<Volume>\d+) (?:- )?(?<Series>.*) (\d+)?",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Batman & Wildcat (1 of 3) // Batman & Wildcat (1 of 3)
new Regex( new Regex(
@"(?<Series>.*(\d{4})?)( |_)(?:\((?<Volume>\d+) of \d+)", @"(?<Series>.*(\d{4})?)( |_)(?:\((?<Volume>\d+) of \d+)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// 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(
@"^(?<Series>.*)(?: |_)v\d+", @"^(?<Series>.*)(?: |_)v\d+",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Amazing Man Comics chapter 25 // Amazing Man Comics chapter 25
new Regex( new Regex(
@"^(?<Series>.*)(?: |_)c(hapter) \d+", @"^(?<Series>.*)(?: |_)c(hapter) \d+",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Amazing Man Comics issue #25 // Amazing Man Comics issue #25
new Regex( new Regex(
@"^(?<Series>.*)(?: |_)i(ssue) #\d+", @"^(?<Series>.*)(?: |_)i(ssue) #\d+",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Batman Wayne Family Adventures - Ep. 001 - Moving In // Batman Wayne Family Adventures - Ep. 001 - Moving In
new Regex( new Regex(
@"^(?<Series>.+?)(\s|_|-)?(?:Ep\.?)(\s|_|-)+\d+", @"^(?<Series>.+?)(\s|_|-)?(?:Ep\.?)(\s|_|-)+\d+",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout), // Batgirl Vol.2000 #57 (December, 2004)
// 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>.+?)(?: \d+)", @"^(?<Series>.+?)Vol\.?\s?#?(?:\d+)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Batman & Robin the Teen Wonder #0 // Batman & Robin the Teen Wonder #0
new Regex( new Regex(
@"^(?<Series>.*)(?: |_)#\d+", @"^(?<Series>.*)(?: |_)#\d+",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout), // 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(
@"^(?<Series>.+?)(?: \d+)",
MatchOptions, RegexTimeout),
// 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>.*)(?: |_)(?<Volume>\d+)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// The First Asterix Frieze (WebP by Doc MaKS) // The First Asterix Frieze (WebP by Doc MaKS)
new Regex( new Regex(
@"^(?<Series>.*)(?: |_)(?!\(\d{4}|\d{4}-\d{2}\))\(", @"^(?<Series>.*)(?: |_)(?!\(\d{4}|\d{4}-\d{2}\))\(",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// spawn-123 (from https://github.com/Girbons/comics-downloader) // spawn-123 (from https://github.com/Girbons/comics-downloader)
new Regex( new Regex(
@"^(?<Series>.+?)-(?<Chapter>\d+)", @"^(?<Series>.+?)-(?<Chapter>\d+)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// MUST BE LAST: Batman & Daredevil - King of New York // MUST BE LAST: Batman & Daredevil - King of New York
new Regex( new Regex(
@"^(?<Series>.*)", @"^(?<Series>.*)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
}; };
private static readonly Regex[] ComicVolumeRegex = new[] private static readonly Regex[] ComicVolumeRegex = new[]
{ {
// // 04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS)
// new Regex(
// @"^(?<Volume>\d+) (- |_)?(?<Series>.*(\d{4})?)( |_)(\(|\d+)",
// MatchOptions,
// RegexTimeout),
// // 01 Spider-Man & Wolverine 01.cbr
// new Regex(
// @"^(?<Volume>\d+) (?:- )?(?<Series>.*) (\d+)?",
// MatchOptions,
// RegexTimeout),
// // Batman & Wildcat (1 of 3)
// new Regex(
// @"(?<Series>.*(\d{4})?)( |_)(?:\((?<Chapter>\d+) of \d+)",
// MatchOptions,
// RegexTimeout),
// 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(
@"^(?<Series>.*)(?: |_)v(?<Volume>\d+)", @"^(?<Series>.*)(?: |_)v(?<Volume>\d+)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout), // Batgirl Vol.2000 #57 (December, 2004)
// Scott Pilgrim 02 - Scott Pilgrim vs. The World (2005) new Regex(
// BUG: Negative lookbehind has to be fixed width @"^(?<Series>.+?)(?:\s|_)vol\.?\s?(?<Volume>\d+)",
// NOTE: The case this is built for does not make much sense. MatchOptions, RegexTimeout),
// new Regex(
// @"^(?<Series>.+?)(?<!c(hapter)|i(ssue))(?<!of)(?: |_)(?<!of )(?<Volume>\d+)",
// MatchOptions,
// RegexTimeout),
// 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(
// @"^(?<Series>.+?)(?<!c(hapter)|i(ssue))(?<!of)(?: (?<Volume>\d+))",
// MatchOptions,
// RegexTimeout),
// // Batman & Robin the Teen Wonder #0
// new Regex(
// @"^(?<Series>.*)(?: |_)#(?<Volume>\d+)",
// MatchOptions,
// RegexTimeout),
}; };
private static readonly Regex[] ComicChapterRegex = new[] private static readonly Regex[] ComicChapterRegex = new[]
@ -386,61 +305,65 @@ namespace API.Parser
// Batman & Wildcat (1 of 3) // Batman & Wildcat (1 of 3)
new Regex( new Regex(
@"(?<Series>.*(\d{4})?)( |_)(?:\((?<Chapter>\d+) of \d+)", @"(?<Series>.*(\d{4})?)( |_)(?:\((?<Chapter>\d+) of \d+)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Batman Beyond 04 (of 6) (1999) // Batman Beyond 04 (of 6) (1999)
new Regex( new Regex(
@"(?<Series>.+?)(?<Chapter>\d+)(\s|_|-)?\(of", @"(?<Series>.+?)(?<Chapter>\d+)(\s|_|-)?\(of",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout), // Batman Beyond 2.0 001 (2013)
new Regex(
@"^(?<Series>.+?\S\.\d) (?<Chapter>\d+)",
MatchOptions, RegexTimeout),
// 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(
@"^(?<Series>.+?)(?: |_)v(?<Volume>\d+)(?: |_)(c? ?)(?<Chapter>(\d+(\.\d)?)-?(\d+(\.\d)?)?)(c? ?)", @"^(?<Series>.+?)(?: |_)v(?<Volume>\d+)(?: |_)(c? ?)(?<Chapter>(\d+(\.\d)?)-?(\d+(\.\d)?)?)(c? ?)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout), // Batman & Robin the Teen Wonder #0
new Regex(
@"^(?<Series>.+?)(?:\s|_)#(?<Chapter>\d+)",
MatchOptions, RegexTimeout),
// Invincible 070.5 - Invincible Returns 1 (2010) (digital) (Minutemen-InnerDemons).cbr // Invincible 070.5 - Invincible Returns 1 (2010) (digital) (Minutemen-InnerDemons).cbr
new Regex( new Regex(
@"^(?<Series>.+?)(?: |_)(c? ?)(?<Chapter>(\d+(\.\d)?)-?(\d+(\.\d)?)?)(c? ?)-", @"^(?<Series>.+?)(?: |_)(c? ?)(?<Chapter>(\d+(\.\d)?)-?(\d+(\.\d)?)?)(c? ?)-",
MatchOptions, RegexTimeout),
// Batgirl Vol.2000 #57 (December, 2004)
new Regex(
@"^(?<Series>.+?)(?:vol\.?\d+)\s#(?<Chapter>\d+)",
MatchOptions, MatchOptions,
RegexTimeout), RegexTimeout),
// 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>.+?)(?: (?<Chapter>\d+))", @"^(?<Series>.+?)(?: (?<Chapter>\d+))",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Batman & Robin the Teen Wonder #0
new Regex(
@"^(?<Series>.+?)(?:\s|_)#(?<Chapter>\d+)",
MatchOptions,
RegexTimeout),
// Saga 001 (2012) (Digital) (Empire-Zone) // Saga 001 (2012) (Digital) (Empire-Zone)
new Regex( new Regex(
@"(?<Series>.+?)(?: |_)(c? ?)(?<Chapter>(\d+(\.\d)?)-?(\d+(\.\d)?)?)\s\(\d{4}", @"(?<Series>.+?)(?: |_)(c? ?)(?<Chapter>(\d+(\.\d)?)-?(\d+(\.\d)?)?)\s\(\d{4}",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Amazing Man Comics chapter 25 // Amazing Man Comics chapter 25
new Regex( new Regex(
@"^(?!Vol)(?<Series>.+?)( |_)c(hapter)( |_)(?<Chapter>\d*)", @"^(?!Vol)(?<Series>.+?)( |_)c(hapter)( |_)(?<Chapter>\d*)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Amazing Man Comics issue #25 // Amazing Man Comics issue #25
new Regex( new Regex(
@"^(?!Vol)(?<Series>.+?)( |_)i(ssue)( |_) #(?<Chapter>\d*)", @"^(?!Vol)(?<Series>.+?)( |_)i(ssue)( |_) #(?<Chapter>\d*)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// spawn-123 (from https://github.com/Girbons/comics-downloader ) // spawn-123 (from https://github.com/Girbons/comics-downloader )
new Regex( new Regex(
@"^(?<Series>.+?)-(?<Chapter>\d+)", @"^(?<Series>.+?)-(?<Chapter>\d+)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout), // Cyberpunk 2077 - Your Voice 01
// new Regex(
// @"^(?<Series>.+?\s?-\s?(?:.+?))(?<Chapter>(\d+(\.\d)?)-?(\d+(\.\d)?)?)$",
// MatchOptions,
// RegexTimeout),
}; };
private static readonly Regex[] ReleaseGroupRegex = new[] private static readonly Regex[] ReleaseGroupRegex = new[]
{ {
// [TrinityBAKumA Finella&anon], [BAA]_, [SlowManga&OverloadScans], [batoto] // [TrinityBAKumA Finella&anon], [BAA]_, [SlowManga&OverloadScans], [batoto]
new Regex(@"(?:\[(?<subgroup>(?!\s).+?(?<!\s))\](?:_|-|\s|\.)?)", new Regex(@"(?:\[(?<subgroup>(?!\s).+?(?<!\s))\](?:_|-|\s|\.)?)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// (Shadowcat-Empire), // (Shadowcat-Empire),
// new Regex(@"(?:\[(?<subgroup>(?!\s).+?(?<!\s))\](?:_|-|\s|\.)?)", // new Regex(@"(?:\[(?<subgroup>(?!\s).+?(?<!\s))\](?:_|-|\s|\.)?)",
// MatchOptions), // MatchOptions),
@ -451,76 +374,62 @@ namespace API.Parser
// Historys Strongest Disciple Kenichi_v11_c90-98.zip, ...c90.5-100.5 // Historys Strongest Disciple Kenichi_v11_c90-98.zip, ...c90.5-100.5
new Regex( new Regex(
@"(\b|_)(c|ch)(\.?\s?)(?<Chapter>(\d+(\.\d)?)-?(\d+(\.\d)?)?)", @"(\b|_)(c|ch)(\.?\s?)(?<Chapter>(\d+(\.\d)?)-?(\d+(\.\d)?)?)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip // [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip
new Regex( new Regex(
@"v\d+\.(?<Chapter>\d+(?:.\d+|-\d+)?)", @"v\d+\.(?<Chapter>\d+(?:.\d+|-\d+)?)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Umineko no Naku Koro ni - Episode 3 - Banquet of the Golden Witch #02.cbz (Rare case, if causes issue remove) // Umineko no Naku Koro ni - Episode 3 - Banquet of the Golden Witch #02.cbz (Rare case, if causes issue remove)
new Regex( new Regex(
@"^(?<Series>.*)(?: |_)#(?<Chapter>\d+)", @"^(?<Series>.*)(?: |_)#(?<Chapter>\d+)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Green Worldz - Chapter 027, Kimi no Koto ga Daidaidaidaidaisuki na 100-nin no Kanojo Chapter 11-10 // Green Worldz - Chapter 027, Kimi no Koto ga Daidaidaidaidaisuki na 100-nin no Kanojo Chapter 11-10
new Regex( new Regex(
@"^(?!Vol)(?<Series>.*)\s?(?<!vol\. )\sChapter\s(?<Chapter>\d+(?:\.?[\d-]+)?)", @"^(?!Vol)(?<Series>.*)\s?(?<!vol\. )\sChapter\s(?<Chapter>\d+(?:\.?[\d-]+)?)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// 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)\.?\s(?<Chapter>\d+(?:.\d+|-\d+)?)(?:\s\(\d{4}\))?(\b|_|-)", @"^(?!Vol)(?<Series>.+?)(?<!Vol)\.?\s(?<Chapter>\d+(?:.\d+|-\d+)?)(?:\s\(\d{4}\))?(\b|_|-)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Tower Of God S01 014 (CBT) (digital).cbz // Tower Of God S01 014 (CBT) (digital).cbz
new Regex( new Regex(
@"(?<Series>.*)\sS(?<Volume>\d+)\s(?<Chapter>\d+(?:.\d+|-\d+)?)", @"(?<Series>.*)\sS(?<Volume>\d+)\s(?<Chapter>\d+(?:.\d+|-\d+)?)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Beelzebub_01_[Noodles].zip, Beelzebub_153b_RHS.zip // Beelzebub_01_[Noodles].zip, Beelzebub_153b_RHS.zip
new Regex( new Regex(
@"^((?!v|vo|vol|Volume).)*(\s|_)(?<Chapter>\.?\d+(?:.\d+|-\d+)?)(?<Part>b)?(\s|_|\[|\()", @"^((?!v|vo|vol|Volume).)*(\s|_)(?<Chapter>\.?\d+(?:.\d+|-\d+)?)(?<Part>b)?(\s|_|\[|\()",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Yumekui-Merry_DKThias_Chapter21.zip // Yumekui-Merry_DKThias_Chapter21.zip
new Regex( new Regex(
@"Chapter(?<Chapter>\d+(-\d+)?)", //(?:.\d+|-\d+)? @"Chapter(?<Chapter>\d+(-\d+)?)", //(?:.\d+|-\d+)?
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// [Hidoi]_Amaenaideyo_MS_vol01_chp02.rar // [Hidoi]_Amaenaideyo_MS_vol01_chp02.rar
new Regex( new Regex(
@"(?<Series>.*)(\s|_)(vol\d+)?(\s|_)Chp\.? ?(?<Chapter>\d+)", @"(?<Series>.*)(\s|_)(vol\d+)?(\s|_)Chp\.? ?(?<Chapter>\d+)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Vol 1 Chapter 2 // Vol 1 Chapter 2
new Regex( new Regex(
@"(?<Volume>((vol|volume|v))?(\s|_)?\.?\d+)(\s|_)(Chp|Chapter)\.?(\s|_)?(?<Chapter>\d+)", @"(?<Volume>((vol|volume|v))?(\s|_)?\.?\d+)(\s|_)(Chp|Chapter)\.?(\s|_)?(?<Chapter>\d+)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
}; };
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(}|\)|\]))",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// 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(( |_)?Edition)?)(\b|_)?", @"(\b|_)(?<Edition>Omnibus(( |_)?Edition)?)(\b|_)?",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// To Love Ru v01 Uncensored (Ch.001-007) // To Love Ru v01 Uncensored (Ch.001-007)
new Regex( new Regex(
@"(\b|_)(?<Edition>Uncensored)(\b|_)", @"(\b|_)(?<Edition>Uncensored)(\b|_)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// AKIRA - c003 (v01) [Full Color] [Darkhorse].cbz // AKIRA - c003 (v01) [Full Color] [Darkhorse].cbz
new Regex( new Regex(
@"(\b|_)(?<Edition>Full(?: |_)Color)(\b|_)?", @"(\b|_)(?<Edition>Full(?: |_)Color)(\b|_)?",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
}; };
private static readonly Regex[] CleanupRegex = private static readonly Regex[] CleanupRegex =
@ -528,18 +437,15 @@ namespace API.Parser
// (), {}, [] // (), {}, []
new Regex( new Regex(
@"(?<Cleanup>(\{\}|\[\]|\(\)))", @"(?<Cleanup>(\{\}|\[\]|\(\)))",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// (Complete) // (Complete)
new Regex( new Regex(
@"(?<Cleanup>(\{Complete\}|\[Complete\]|\(Complete\)))", @"(?<Cleanup>(\{Complete\}|\[Complete\]|\(Complete\)))",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
// Anything in parenthesis // Anything in parenthesis
new Regex( new Regex(
@"\(.*\)", @"\(.*\)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout),
}; };
private static readonly Regex[] MangaSpecialRegex = private static readonly Regex[] MangaSpecialRegex =
@ -547,15 +453,21 @@ namespace API.Parser
// All Keywords, does not account for checking if contains volume/chapter identification. Parser.Parse() will handle. // All Keywords, does not account for checking if contains volume/chapter identification. Parser.Parse() will handle.
new Regex( new Regex(
@"(?<Special>Specials?|OneShot|One\-Shot|Omake|Extra( Chapter)?|Art Collection|Side( |_)Stories|Bonus)", @"(?<Special>Specials?|OneShot|One\-Shot|Omake|Extra( Chapter)?|Art Collection|Side( |_)Stories|Bonus)",
MatchOptions, MatchOptions, RegexTimeout),
RegexTimeout), };
private static readonly Regex[] ComicSpecialRegex =
{
// All Keywords, does not account for checking if contains volume/chapter identification. Parser.Parse() will handle.
new Regex(
@"(?<Special>Specials?|OneShot|One\-Shot|Extra( Chapter)?|Book \d.+?|Compendium \d.+?|Omnibus \d.+?|[_\s\-]TPB[_\s\-]|FCBD \d.+?|Absolute \d.+?|Preview \d.+?|Art Collection|Side( |_)Stories|Bonus)",
MatchOptions, RegexTimeout),
}; };
// If SP\d+ is in the filename, we force treat it as a special regardless if volume or chapter might have been found. // If SP\d+ is in the filename, we force treat it as a special regardless if volume or chapter might have been found.
private static readonly Regex SpecialMarkerRegex = new Regex( private static readonly Regex SpecialMarkerRegex = new Regex(
@"(?<Special>SP\d+)", @"(?<Special>SP\d+)",
MatchOptions, MatchOptions, RegexTimeout
RegexTimeout
); );
@ -569,7 +481,7 @@ namespace API.Parser
/// <returns><see cref="ParserInfo"/> or null if Series was empty</returns> /// <returns><see cref="ParserInfo"/> or null if Series was empty</returns>
public static ParserInfo Parse(string filePath, string rootPath, LibraryType type = LibraryType.Manga) public static ParserInfo Parse(string filePath, string rootPath, LibraryType type = LibraryType.Manga)
{ {
var fileName = Path.GetFileName(filePath); var fileName = Path.GetFileNameWithoutExtension(filePath);
ParserInfo ret; ParserInfo ret;
if (IsEpub(filePath)) if (IsEpub(filePath))
@ -579,7 +491,7 @@ namespace API.Parser
Chapters = ParseChapter(fileName) ?? ParseComicChapter(fileName), Chapters = ParseChapter(fileName) ?? ParseComicChapter(fileName),
Series = ParseSeries(fileName) ?? ParseComicSeries(fileName), Series = ParseSeries(fileName) ?? ParseComicSeries(fileName),
Volumes = ParseVolume(fileName) ?? ParseComicVolume(fileName), Volumes = ParseVolume(fileName) ?? ParseComicVolume(fileName),
Filename = fileName, Filename = Path.GetFileName(filePath),
Format = ParseFormat(filePath), Format = ParseFormat(filePath),
FullFilePath = filePath FullFilePath = filePath
}; };
@ -591,14 +503,14 @@ namespace API.Parser
Chapters = type == LibraryType.Manga ? ParseChapter(fileName) : ParseComicChapter(fileName), Chapters = type == LibraryType.Manga ? ParseChapter(fileName) : ParseComicChapter(fileName),
Series = type == LibraryType.Manga ? ParseSeries(fileName) : ParseComicSeries(fileName), Series = type == LibraryType.Manga ? ParseSeries(fileName) : ParseComicSeries(fileName),
Volumes = type == LibraryType.Manga ? ParseVolume(fileName) : ParseComicVolume(fileName), Volumes = type == LibraryType.Manga ? ParseVolume(fileName) : ParseComicVolume(fileName),
Filename = fileName, Filename = Path.GetFileName(filePath),
Format = ParseFormat(filePath), Format = ParseFormat(filePath),
Title = Path.GetFileNameWithoutExtension(fileName), Title = Path.GetFileNameWithoutExtension(fileName),
FullFilePath = filePath FullFilePath = filePath
}; };
} }
if (IsImage(filePath) && IsCoverImage(fileName)) return null; if (IsImage(filePath) && IsCoverImage(filePath)) return null;
if (IsImage(filePath)) if (IsImage(filePath))
{ {
@ -617,7 +529,7 @@ namespace API.Parser
var edition = ParseEdition(fileName); var edition = ParseEdition(fileName);
if (!string.IsNullOrEmpty(edition)) if (!string.IsNullOrEmpty(edition))
{ {
ret.Series = CleanTitle(ret.Series.Replace(edition, "")); ret.Series = CleanTitle(ret.Series.Replace(edition, ""), type is LibraryType.Comic);
ret.Edition = edition; ret.Edition = edition;
} }
@ -642,11 +554,11 @@ namespace API.Parser
if (string.IsNullOrEmpty(ret.Series)) if (string.IsNullOrEmpty(ret.Series))
{ {
ret.Series = CleanTitle(fileName); ret.Series = CleanTitle(fileName, type is LibraryType.Comic);
} }
// Pdfs may have .pdf in the series name, remove that // Pdfs may have .pdf in the series name, remove that
if (IsPdf(fileName) && ret.Series.ToLower().EndsWith(".pdf")) if (IsPdf(filePath) && ret.Series.ToLower().EndsWith(".pdf"))
{ {
ret.Series = ret.Series.Substring(0, ret.Series.Length - ".pdf".Length); ret.Series = ret.Series.Substring(0, ret.Series.Length - ".pdf".Length);
} }
@ -690,7 +602,7 @@ namespace API.Parser
if ((string.IsNullOrEmpty(series) && i == fallbackFolders.Count - 1)) if ((string.IsNullOrEmpty(series) && i == fallbackFolders.Count - 1))
{ {
ret.Series = CleanTitle(folder); ret.Series = CleanTitle(folder, type is LibraryType.Comic);
break; break;
} }
@ -767,6 +679,23 @@ namespace API.Parser
return string.Empty; return string.Empty;
} }
public static string ParseComicSpecial(string filePath)
{
foreach (var regex in ComicSpecialRegex)
{
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)
@ -792,7 +721,7 @@ namespace API.Parser
{ {
if (match.Groups["Series"].Success && match.Groups["Series"].Value != string.Empty) if (match.Groups["Series"].Success && match.Groups["Series"].Value != string.Empty)
{ {
return CleanTitle(match.Groups["Series"].Value); return CleanTitle(match.Groups["Series"].Value, true);
} }
} }
} }
@ -912,12 +841,30 @@ namespace API.Parser
{ {
if (match.Success) if (match.Success)
{ {
title = title.Replace(match.Value, "").Trim(); title = title.Replace(match.Value, string.Empty).Trim();
} }
} }
} }
// TODO: Since we have loops like this, think about using a method
foreach (var regex in MangaEditionRegex) foreach (var regex in MangaEditionRegex)
{
var matches = regex.Matches(title);
foreach (Match match in matches)
{
if (match.Success)
{
title = title.Replace(match.Value, string.Empty).Trim();
}
}
}
return title;
}
private static string RemoveMangaSpecialTags(string title)
{
foreach (var regex in MangaSpecialRegex)
{ {
var matches = regex.Matches(title); var matches = regex.Matches(title);
foreach (Match match in matches) foreach (Match match in matches)
@ -932,9 +879,9 @@ namespace API.Parser
return title; return title;
} }
private static string RemoveSpecialTags(string title) private static string RemoveComicSpecialTags(string title)
{ {
foreach (var regex in MangaSpecialRegex) foreach (var regex in ComicSpecialRegex)
{ {
var matches = regex.Matches(title); var matches = regex.Matches(title);
foreach (Match match in matches) foreach (Match match in matches)
@ -958,14 +905,16 @@ namespace API.Parser
/// </example> /// </example>
/// </summary> /// </summary>
/// <param name="title"></param> /// <param name="title"></param>
/// <param name="isComic"></param>
/// <returns></returns> /// <returns></returns>
public static string CleanTitle(string title) public static string CleanTitle(string title, bool isComic = false)
{ {
title = RemoveReleaseGroup(title); title = RemoveReleaseGroup(title);
title = RemoveEditionTagHolders(title); title = RemoveEditionTagHolders(title);
title = RemoveSpecialTags(title); title = isComic ? RemoveComicSpecialTags(title) : RemoveMangaSpecialTags(title);
title = title.Replace("_", " ").Trim(); title = title.Replace("_", " ").Trim();
if (title.EndsWith("-") || title.EndsWith(",")) if (title.EndsWith("-") || title.EndsWith(","))

View File

@ -22,6 +22,8 @@ import { MemberService } from 'src/app/_services/member.service';
import { ReadingDirection } from 'src/app/_models/preferences/reading-direction'; import { ReadingDirection } from 'src/app/_models/preferences/reading-direction';
import { ScrollService } from 'src/app/scroll.service'; import { ScrollService } from 'src/app/scroll.service';
import { MangaFormat } from 'src/app/_models/manga-format'; import { MangaFormat } from 'src/app/_models/manga-format';
import { LibraryService } from 'src/app/_services/library.service';
import { LibraryType } from 'src/app/_models/library';
interface PageStyle { interface PageStyle {
@ -169,6 +171,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
* Last seen progress part path * Last seen progress part path
*/ */
lastSeenScrollPartPath: string = ''; lastSeenScrollPartPath: string = '';
/**
* Library Type used for rendering chapter or issue
*/
libraryType: LibraryType = LibraryType.Book;
/** /**
* Hack: Override background color for reader and restore it onDestroy * Hack: Override background color for reader and restore it onDestroy
@ -223,7 +229,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
private seriesService: SeriesService, private readerService: ReaderService, private location: Location, private seriesService: SeriesService, private readerService: ReaderService, private location: Location,
private renderer: Renderer2, private navService: NavService, private toastr: ToastrService, private renderer: Renderer2, private navService: NavService, private toastr: ToastrService,
private domSanitizer: DomSanitizer, private bookService: BookService, private memberService: MemberService, private domSanitizer: DomSanitizer, private bookService: BookService, private memberService: MemberService,
private scrollService: ScrollService, private utilityService: UtilityService) { private scrollService: ScrollService, private utilityService: UtilityService, private libraryService: LibraryService) {
this.navService.hideNavBar(); this.navService.hideNavBar();
this.darkModeStyleElem = this.renderer.createElement('style'); this.darkModeStyleElem = this.renderer.createElement('style');
@ -421,6 +427,11 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.continuousChaptersStack.push(this.chapterId); this.continuousChaptersStack.push(this.chapterId);
this.libraryService.getLibraryType(this.libraryId).pipe(take(1)).subscribe(type => {
this.libraryType = type;
});
if (this.pageNum >= this.maxPages) { if (this.pageNum >= this.maxPages) {
@ -529,10 +540,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
const newRoute = this.readerService.getNextChapterUrl(this.router.url, this.chapterId, this.incognitoMode, this.readingListMode, this.readingListId); const newRoute = this.readerService.getNextChapterUrl(this.router.url, this.chapterId, this.incognitoMode, this.readingListMode, this.readingListId);
window.history.replaceState({}, '', newRoute); window.history.replaceState({}, '', newRoute);
this.init(); this.init();
this.toastr.info(direction + ' chapter loaded', '', {timeOut: 3000}); this.toastr.info(direction + ' ' + this.utilityService.formatChapterName(this.libraryType).toLowerCase() + ' loaded', '', {timeOut: 3000});
} else { } else {
// This will only happen if no actual chapter can be found // This will only happen if no actual chapter can be found
this.toastr.warning('Could not find ' + direction + ' chapter'); this.toastr.warning('Could not find ' + direction.toLowerCase() + ' ' + this.utilityService.formatChapterName(this.libraryType).toLowerCase());
this.isLoading = false; this.isLoading = false;
if (direction === 'Prev') { if (direction === 'Prev') {
this.prevPageDisabled = true; this.prevPageDisabled = true;

View File

@ -1,7 +1,10 @@
<div *ngIf="data !== undefined"> <div *ngIf="data !== undefined">
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title" id="modal-basic-title"> <h4 *ngIf="libraryType !== LibraryType.Comic else comicHeader" class="modal-title" id="modal-basic-title">
{{parentName}} - {{data.number != 0 ? (isChapter ? 'Chapter ' : 'Volume ') + data.number : 'Special'}} Details</h4> {{parentName}} - {{data.number != 0 ? (isChapter ? 'Chapter ' : 'Volume ') + data.number : 'Special'}} Details</h4>
<ng-template #comicHeader><h4 class="modal-title" id="modal-basic-title">
{{parentName}} - {{data.number != 0 ? (isChapter ? 'Issue #' : 'Volume ') + data.number : 'Special'}} Details</h4>
</ng-template>
<button type="button" class="close" aria-label="Close" (click)="close()"> <button type="button" class="close" aria-label="Close" (click)="close()">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
@ -27,18 +30,19 @@
</div> </div>
</ng-container> </ng-container>
<h4 *ngIf="!utilityService.isChapter(data)">Chapters</h4> <h4 *ngIf="!utilityService.isChapter(data)">{{utilityService.formatChapterName(libraryType) + 's'}}</h4>
<ul class="list-unstyled"> <ul class="list-unstyled">
<li class="media my-4" *ngFor="let chapter of chapters"> <li class="media my-4" *ngFor="let chapter of chapters">
<a (click)="readChapter(chapter)" href="javascript:void(0);" title="Read Chapter {{chapter.number}}"> <a (click)="readChapter(chapter)" href="javascript:void(0);" title="Read {{libraryType !== LibraryType.Comic ? 'Chapter ' : 'Issue #'}} {{chapter.number}}">
<img class="mr-3" style="width: 74px" [src]="chapter.coverImage"> <img class="mr-3" style="width: 74px" [src]="chapter.coverImage">
</a> </a>
<div class="media-body"> <div class="media-body">
<h5 class="mt-0 mb-1"> <h5 class="mt-0 mb-1">
<span *ngIf="chapter.number !== '0'; else specialHeader"> <span *ngIf="chapter.number !== '0'; else specialHeader">
<span class=""> <span >
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions" [labelBy]="'Chapter' + formatChapterNumber(chapter)"></app-card-actionables>&nbsp; <app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions" [labelBy]="utilityService.formatChapterName(libraryType, true, true) + formatChapterNumber(chapter)"></app-card-actionables>&nbsp;
</span>Chapter {{formatChapterNumber(chapter)}} {{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
</span>
<span class="badge badge-primary badge-pill"> <span class="badge badge-primary badge-pill">
<span *ngIf="chapter.pagesRead > 0 && chapter.pagesRead < chapter.pages">{{chapter.pagesRead}} / {{chapter.pages}}</span> <span *ngIf="chapter.pagesRead > 0 && chapter.pagesRead < chapter.pages">{{chapter.pagesRead}} / {{chapter.pages}}</span>
<span *ngIf="chapter.pagesRead === 0">UNREAD</span> <span *ngIf="chapter.pagesRead === 0">UNREAD</span>

View File

@ -13,6 +13,8 @@ import { ActionService } from 'src/app/_services/action.service';
import { ImageService } from 'src/app/_services/image.service'; import { ImageService } from 'src/app/_services/image.service';
import { UploadService } from 'src/app/_services/upload.service'; import { UploadService } from 'src/app/_services/upload.service';
import { ChangeCoverImageModalComponent } from '../change-cover-image/change-cover-image-modal.component'; import { ChangeCoverImageModalComponent } from '../change-cover-image/change-cover-image-modal.component';
import { LibraryType } from '../../../_models/library';
import { LibraryService } from '../../../_services/library.service';
@ -39,12 +41,16 @@ export class CardDetailsModalComponent implements OnInit {
isAdmin: boolean = false; isAdmin: boolean = false;
actions: ActionItem<any>[] = []; actions: ActionItem<any>[] = [];
chapterActions: ActionItem<Chapter>[] = []; chapterActions: ActionItem<Chapter>[] = [];
libraryType: LibraryType = LibraryType.Manga;
get LibraryType(): typeof LibraryType {
return LibraryType;
}
constructor(private modalService: NgbModal, public modal: NgbActiveModal, public utilityService: UtilityService, constructor(private modalService: NgbModal, public modal: NgbActiveModal, public utilityService: UtilityService,
public imageService: ImageService, private uploadService: UploadService, private toastr: ToastrService, public imageService: ImageService, private uploadService: UploadService, private toastr: ToastrService,
private accountService: AccountService, private actionFactoryService: ActionFactoryService, private accountService: AccountService, private actionFactoryService: ActionFactoryService,
private actionService: ActionService, private router: Router) { } private actionService: ActionService, private router: Router, private libraryService: LibraryService) { }
ngOnInit(): void { ngOnInit(): void {
this.isChapter = this.utilityService.isChapter(this.data); this.isChapter = this.utilityService.isChapter(this.data);
@ -55,6 +61,10 @@ export class CardDetailsModalComponent implements OnInit {
} }
}); });
this.libraryService.getLibraryType(this.libraryId).subscribe(type => {
this.libraryType = type;
});
this.chapterActions = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this)).filter(item => item.action !== Action.Edit); this.chapterActions = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this)).filter(item => item.action !== Action.Edit);
if (this.isChapter) { if (this.isChapter) {
@ -94,7 +104,7 @@ export class CardDetailsModalComponent implements OnInit {
const chapter = this.utilityService.asChapter(this.data) const chapter = this.utilityService.asChapter(this.data)
chapter.coverImage = this.imageService.getChapterCoverImage(chapter.id); chapter.coverImage = this.imageService.getChapterCoverImage(chapter.id);
modalRef.componentInstance.chapter = chapter; modalRef.componentInstance.chapter = chapter;
modalRef.componentInstance.title = 'Select ' + (chapter.isSpecial ? '' : 'Chapter ') + chapter.range + '\'s Cover'; modalRef.componentInstance.title = 'Select ' + (chapter.isSpecial ? '' : this.utilityService.formatChapterName(this.libraryType, false, true)) + chapter.range + '\'s Cover';
} else { } else {
const volume = this.utilityService.asVolume(this.data); const volume = this.utilityService.asVolume(this.data);
const chapters = volume.chapters; const chapters = volume.chapters;

View File

@ -12,7 +12,7 @@ import { ScalingOption } from '../_models/preferences/scaling-option';
import { PageSplitOption } from '../_models/preferences/page-split-option'; import { PageSplitOption } from '../_models/preferences/page-split-option';
import { forkJoin, ReplaySubject, Subject } from 'rxjs'; import { forkJoin, ReplaySubject, Subject } from 'rxjs';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { KEY_CODES } from '../shared/_services/utility.service'; import { KEY_CODES, UtilityService } from '../shared/_services/utility.service';
import { CircularArray } from '../shared/data-structures/circular-array'; import { CircularArray } from '../shared/data-structures/circular-array';
import { MemberService } from '../_services/member.service'; import { MemberService } from '../_services/member.service';
import { Stack } from '../shared/data-structures/stack'; import { Stack } from '../shared/data-structures/stack';
@ -23,6 +23,8 @@ import { COLOR_FILTER, FITTING_OPTION, PAGING_DIRECTION, SPLIT_PAGE_PART } from
import { Preferences, scalingOptions } from '../_models/preferences/preferences'; import { Preferences, scalingOptions } from '../_models/preferences/preferences';
import { READER_MODE } from '../_models/preferences/reader-mode'; import { READER_MODE } from '../_models/preferences/reader-mode';
import { MangaFormat } from '../_models/manga-format'; import { MangaFormat } from '../_models/manga-format';
import { LibraryService } from '../_services/library.service';
import { LibraryType } from '../_models/library';
const PREFETCH_PAGES = 5; const PREFETCH_PAGES = 5;
@ -205,6 +207,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
* Tracks if the first page is rendered or not. This is used to keep track of Automatic Scaling and adjusting decision after first page dimensions load up. * Tracks if the first page is rendered or not. This is used to keep track of Automatic Scaling and adjusting decision after first page dimensions load up.
*/ */
firstPageRendered: boolean = false; firstPageRendered: boolean = false;
/**
* Library Type used for rendering chapter or issue
*/
libraryType: LibraryType = LibraryType.Manga;
private readonly onDestroy = new Subject<void>(); private readonly onDestroy = new Subject<void>();
@ -260,7 +266,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService, constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService,
public readerService: ReaderService, private location: Location, public readerService: ReaderService, private location: Location,
private formBuilder: FormBuilder, private navService: NavService, private formBuilder: FormBuilder, private navService: NavService,
private toastr: ToastrService, private memberService: MemberService) { private toastr: ToastrService, private memberService: MemberService,
private libraryService: LibraryService, private utilityService: UtilityService) {
this.navService.hideNavBar(); this.navService.hideNavBar();
} }
@ -400,7 +407,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
forkJoin({ forkJoin({
progress: this.readerService.getProgress(this.chapterId), progress: this.readerService.getProgress(this.chapterId),
chapterInfo: this.readerService.getChapterInfo(this.chapterId), chapterInfo: this.readerService.getChapterInfo(this.chapterId),
bookmarks: this.readerService.getBookmarks(this.chapterId) bookmarks: this.readerService.getBookmarks(this.chapterId),
}).pipe(take(1)).subscribe(results => { }).pipe(take(1)).subscribe(results => {
if (this.readingListMode && results.chapterInfo.seriesFormat === MangaFormat.EPUB) { if (this.readingListMode && results.chapterInfo.seriesFormat === MangaFormat.EPUB) {
@ -425,7 +432,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
newOptions.ceil = this.maxPages - 1; // We -1 so that the slider UI shows us hitting the end, since visually we +1 everything. newOptions.ceil = this.maxPages - 1; // We -1 so that the slider UI shows us hitting the end, since visually we +1 everything.
this.pageOptions = newOptions; this.pageOptions = newOptions;
this.updateTitle(results.chapterInfo); this.libraryService.getLibraryType(results.chapterInfo.libraryId).pipe(take(1)).subscribe(type => {
this.libraryType = type;
this.updateTitle(results.chapterInfo, type);
});
// From bookmarks, create map of pages to make lookup time O(1) // From bookmarks, create map of pages to make lookup time O(1)
this.bookmarks = {}; this.bookmarks = {};
@ -479,7 +491,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
} }
} }
updateTitle(chapterInfo: ChapterInfo) { updateTitle(chapterInfo: ChapterInfo, type: LibraryType) {
this.title = chapterInfo.seriesName; this.title = chapterInfo.seriesName;
if (chapterInfo.chapterTitle.length > 0) { if (chapterInfo.chapterTitle.length > 0) {
this.title += ' - ' + chapterInfo.chapterTitle; this.title += ' - ' + chapterInfo.chapterTitle;
@ -489,12 +501,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
if (chapterInfo.isSpecial && chapterInfo.volumeNumber === '0') { if (chapterInfo.isSpecial && chapterInfo.volumeNumber === '0') {
this.subtitle = chapterInfo.fileName; this.subtitle = chapterInfo.fileName;
} else if (!chapterInfo.isSpecial && chapterInfo.volumeNumber === '0') { } else if (!chapterInfo.isSpecial && chapterInfo.volumeNumber === '0') {
this.subtitle = 'Chapter ' + chapterInfo.chapterNumber; this.subtitle = this.utilityService.formatChapterName(type, true, true) + chapterInfo.chapterNumber;
} else { } else {
this.subtitle = 'Volume ' + chapterInfo.volumeNumber; this.subtitle = 'Volume ' + chapterInfo.volumeNumber;
if (chapterInfo.chapterNumber !== '0') { if (chapterInfo.chapterNumber !== '0') {
this.subtitle += ' Chapter ' + chapterInfo.chapterNumber; this.subtitle += ' ' + this.utilityService.formatChapterName(type, true, true) + chapterInfo.chapterNumber;
} }
} }
} }
@ -764,10 +776,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
const newRoute = this.readerService.getNextChapterUrl(this.router.url, this.chapterId, this.incognitoMode, this.readingListMode, this.readingListId); const newRoute = this.readerService.getNextChapterUrl(this.router.url, this.chapterId, this.incognitoMode, this.readingListMode, this.readingListId);
window.history.replaceState({}, '', newRoute); window.history.replaceState({}, '', newRoute);
this.init(); this.init();
this.toastr.info(direction + ' chapter loaded', '', {timeOut: 3000}); this.toastr.info(direction + ' ' + this.utilityService.formatChapterName(this.libraryType).toLowerCase() + ' loaded', '', {timeOut: 3000});
} else { } else {
// This will only happen if no actual chapter can be found // This will only happen if no actual chapter can be found
this.toastr.warning('Could not find ' + direction.toLowerCase() + ' chapter'); this.toastr.warning('Could not find ' + direction.toLowerCase() + ' ' + this.utilityService.formatChapterName(this.libraryType).toLowerCase());
this.isLoading = false; this.isLoading = false;
if (direction === 'Prev') { if (direction === 'Prev') {
this.prevPageDisabled = true; this.prevPageDisabled = true;

View File

@ -4,6 +4,7 @@ import { ToastrService } from 'ngx-toastr';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { ConfirmService } from 'src/app/shared/confirm.service'; import { ConfirmService } from 'src/app/shared/confirm.service';
import { UtilityService } from 'src/app/shared/_services/utility.service'; import { UtilityService } from 'src/app/shared/_services/utility.service';
import { LibraryType } from 'src/app/_models/library';
import { MangaFormat } from 'src/app/_models/manga-format'; import { MangaFormat } from 'src/app/_models/manga-format';
import { ReadingList, ReadingListItem } from 'src/app/_models/reading-list'; import { ReadingList, ReadingListItem } from 'src/app/_models/reading-list';
import { AccountService } from 'src/app/_services/account.service'; import { AccountService } from 'src/app/_services/account.service';
@ -12,6 +13,8 @@ import { ActionService } from 'src/app/_services/action.service';
import { ImageService } from 'src/app/_services/image.service'; import { ImageService } from 'src/app/_services/image.service';
import { ReadingListService } from 'src/app/_services/reading-list.service'; import { ReadingListService } from 'src/app/_services/reading-list.service';
import { IndexUpdateEvent, ItemRemoveEvent } from '../dragable-ordered-list/dragable-ordered-list.component'; import { IndexUpdateEvent, ItemRemoveEvent } from '../dragable-ordered-list/dragable-ordered-list.component';
import { LibraryService } from '../../_services/library.service';
import { forkJoin } from 'rxjs';
@Component({ @Component({
selector: 'app-reading-list-detail', selector: 'app-reading-list-detail',
@ -19,7 +22,6 @@ import { IndexUpdateEvent, ItemRemoveEvent } from '../dragable-ordered-list/drag
styleUrls: ['./reading-list-detail.component.scss'] styleUrls: ['./reading-list-detail.component.scss']
}) })
export class ReadingListDetailComponent implements OnInit { export class ReadingListDetailComponent implements OnInit {
items: Array<ReadingListItem> = []; items: Array<ReadingListItem> = [];
listId!: number; listId!: number;
readingList!: ReadingList; readingList!: ReadingList;
@ -32,6 +34,7 @@ export class ReadingListDetailComponent implements OnInit {
hasDownloadingRole: boolean = false; hasDownloadingRole: boolean = false;
downloadInProgress: boolean = false; downloadInProgress: boolean = false;
libraryTypes: {[key: number]: LibraryType} = {};
get MangaFormat(): typeof MangaFormat { get MangaFormat(): typeof MangaFormat {
return MangaFormat; return MangaFormat;
@ -39,7 +42,8 @@ export class ReadingListDetailComponent implements OnInit {
constructor(private route: ActivatedRoute, private router: Router, private readingListService: ReadingListService, constructor(private route: ActivatedRoute, private router: Router, private readingListService: ReadingListService,
private actionService: ActionService, private actionFactoryService: ActionFactoryService, public utilityService: UtilityService, private actionService: ActionService, private actionFactoryService: ActionFactoryService, public utilityService: UtilityService,
public imageService: ImageService, private accountService: AccountService, private toastr: ToastrService, private confirmService: ConfirmService) {} public imageService: ImageService, private accountService: AccountService, private toastr: ToastrService,
private confirmService: ConfirmService, private libraryService: LibraryService) {}
ngOnInit(): void { ngOnInit(): void {
const listId = this.route.snapshot.paramMap.get('id'); const listId = this.route.snapshot.paramMap.get('id');
@ -51,7 +55,21 @@ export class ReadingListDetailComponent implements OnInit {
this.listId = parseInt(listId, 10); this.listId = parseInt(listId, 10);
this.readingListService.getReadingList(this.listId).subscribe(readingList => { this.libraryService.getLibraries().subscribe(libs => {
});
forkJoin([
this.libraryService.getLibraries(),
this.readingListService.getReadingList(this.listId)
]).subscribe(results => {
const libraries = results[0];
const readingList = results[1];
libraries.forEach(lib => {
this.libraryTypes[lib.id] = lib.type;
});
if (readingList == null) { if (readingList == null) {
// The list doesn't exist // The list doesn't exist
this.toastr.error('This list doesn\'t exist.'); this.toastr.error('This list doesn\'t exist.');
@ -81,7 +99,6 @@ export class ReadingListDetailComponent implements OnInit {
} }
performAction(action: ActionItem<any>) { performAction(action: ActionItem<any>) {
// TODO: Try to move performAction into the actionables component. (have default handler in the component, allow for overridding to pass additional context)
if (typeof action.callback === 'function') { if (typeof action.callback === 'function') {
action.callback(action.action, this.readingList); action.callback(action.action, this.readingList);
} }
@ -119,7 +136,7 @@ export class ReadingListDetailComponent implements OnInit {
return 'Volume ' + this.utilityService.cleanSpecialTitle(item.chapterNumber); return 'Volume ' + this.utilityService.cleanSpecialTitle(item.chapterNumber);
} }
return 'Chapter ' + item.chapterNumber; return this.utilityService.formatChapterName(this.libraryTypes[item.libraryId], true, true) + item.chapterNumber;
} }
orderUpdated(event: IndexUpdateEvent) { orderUpdated(event: IndexUpdateEvent) {

View File

@ -112,7 +112,7 @@
</ng-template> </ng-template>
</li> </li>
<li [ngbNavItem]="2" *ngIf="hasNonSpecialVolumeChapters"> <li [ngbNavItem]="2" *ngIf="hasNonSpecialVolumeChapters">
<a ngbNavLink>Volumes/Chapters</a> <a ngbNavLink>{{utilityService.formatChapterName(libraryType) + 's'}}</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<div class="row no-gutters"> <div class="row no-gutters">
<div *ngFor="let volume of volumes; let idx = index; trackBy: trackByVolumeIdentity"> <div *ngFor="let volume of volumes; let idx = index; trackBy: trackByVolumeIdentity">
@ -121,7 +121,7 @@
[read]="volume.pagesRead" [total]="volume.pages" [actions]="volumeActions" (selection)="bulkSelectionService.handleCardSelection('volume', idx, volumes.length, $event)" [selected]="bulkSelectionService.isCardSelected('volume', idx)" [allowSelection]="true"></app-card-item> [read]="volume.pagesRead" [total]="volume.pages" [actions]="volumeActions" (selection)="bulkSelectionService.handleCardSelection('volume', idx, volumes.length, $event)" [selected]="bulkSelectionService.isCardSelected('volume', idx)" [allowSelection]="true"></app-card-item>
</div> </div>
<div *ngFor="let chapter of chapters; let idx = index; trackBy: trackByChapterIdentity"> <div *ngFor="let chapter of chapters; let idx = index; trackBy: trackByChapterIdentity">
<app-card-item class="col-auto" *ngIf="!chapter.isSpecial" [entity]="chapter" [title]="'Chapter ' + chapter.range" (click)="openChapter(chapter)" <app-card-item class="col-auto" *ngIf="!chapter.isSpecial" [entity]="chapter" [title]="utilityService.formatChapterName(libraryType, true, true) + chapter.range" (click)="openChapter(chapter)"
[imageUrl]="imageService.getChapterCoverImage(chapter.id) + '&offset=' + coverImageOffset" [imageUrl]="imageService.getChapterCoverImage(chapter.id) + '&offset=' + coverImageOffset"
[read]="chapter.pagesRead" [total]="chapter.pages" [actions]="chapterActions" (selection)="bulkSelectionService.handleCardSelection('chapter', idx, chapters.length, $event)" [selected]="bulkSelectionService.isCardSelected('chapter', idx)" [allowSelection]="true"></app-card-item> [read]="chapter.pagesRead" [total]="chapter.pages" [actions]="chapterActions" (selection)="bulkSelectionService.handleCardSelection('chapter', idx, chapters.length, $event)" [selected]="bulkSelectionService.isCardSelected('chapter', idx)" [allowSelection]="true"></app-card-item>
</div> </div>

View File

@ -1,5 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Chapter } from 'src/app/_models/chapter'; import { Chapter } from 'src/app/_models/chapter';
import { LibraryType } from 'src/app/_models/library';
import { MangaFormat } from 'src/app/_models/manga-format'; import { MangaFormat } from 'src/app/_models/manga-format';
import { Series } from 'src/app/_models/series'; import { Series } from 'src/app/_models/series';
import { Volume } from 'src/app/_models/volume'; import { Volume } from 'src/app/_models/volume';
@ -56,6 +57,27 @@ export class UtilityService {
return this.mangaFormatKeys.filter(item => MangaFormat[format] === item)[0]; return this.mangaFormatKeys.filter(item => MangaFormat[format] === item)[0];
} }
/**
* Formats a Chapter name based on the library it's in
* @param libraryType
* @param includeHash For comics only, includes a # which is used for numbering on cards
* @param includeSpace Add a space at the end of the string. if includeHash and includeSpace are true, only hash will be at the end.
* @returns
*/
formatChapterName(libraryType: LibraryType, includeHash: boolean = false, includeSpace: boolean = false) {
switch(libraryType) {
case LibraryType.Book:
return 'Book' + (includeSpace ? ' ' : '');
case LibraryType.Comic:
if (includeHash) {
return 'Issue #';
}
return 'Issue' + (includeSpace ? ' ' : '');
case LibraryType.Manga:
return 'Chapter' + (includeSpace ? ' ' : '');
}
}
cleanSpecialTitle(title: string) { cleanSpecialTitle(title: string) {
let cleaned = title.replace(/_/g, ' ').replace(/SP\d+/g, '').trim(); let cleaned = title.replace(/_/g, ' ').replace(/SP\d+/g, '').trim();
cleaned = cleaned.substring(0, cleaned.lastIndexOf('.')); cleaned = cleaned.substring(0, cleaned.lastIndexOf('.'));