From 3293e5b4249d11410209405ba6c0db8019866da9 Mon Sep 17 00:00:00 2001 From: Robbie Davis Date: Thu, 7 Oct 2021 10:49:13 -0400 Subject: [PATCH] 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 --- API.Tests/Parser/ComicParserTests.cs | 102 ++++- API.Tests/Parser/ParserTest.cs | 16 +- API/Parser/Parser.cs | 409 ++++++++---------- .../book-reader/book-reader.component.ts | 17 +- .../card-details-modal.component.html | 16 +- .../card-details-modal.component.ts | 14 +- .../manga-reader/manga-reader.component.ts | 30 +- .../reading-list-detail.component.ts | 27 +- .../series-detail.component.html | 4 +- .../app/shared/_services/utility.service.ts | 22 + 10 files changed, 392 insertions(+), 265 deletions(-) diff --git a/API.Tests/Parser/ComicParserTests.cs b/API.Tests/Parser/ComicParserTests.cs index d1d8209e7..ce9f14065 100644 --- a/API.Tests/Parser/ComicParserTests.cs +++ b/API.Tests/Parser/ComicParserTests.cs @@ -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 { public class ComicParserTests { + private readonly ITestOutputHelper _testOutputHelper; + + public ComicParserTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + [Theory] [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")] @@ -29,7 +41,21 @@ namespace API.Tests.Parser [InlineData("Batman Wayne Family Adventures - Ep. 001 - Moving In", "Batman Wayne Family Adventures")] [InlineData("Saga 001 (2012) (Digital) (Empire-Zone).cbr", "Saga")] [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 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) { 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("Cyberpunk 2077 - Trauma Team 04.cbz", "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) { 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("Saga 001 (2012) (Digital) (Empire-Zone)", "1")] [InlineData("spawn-123", "123")] + [InlineData("Spawn 062 (1997) (digital) (TLK-EMPIRE-HD).cbr", "62")] [InlineData("Batman Beyond 04 (of 6) (1999)", "4")] [InlineData("Invincible 052 (c2c) (2008) (Minutemen-TheCouple)", "52")] [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) { 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(); + 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 ✓"); + } + } + } } diff --git a/API.Tests/Parser/ParserTest.cs b/API.Tests/Parser/ParserTest.cs index 6660ba2af..039beaf91 100644 --- a/API.Tests/Parser/ParserTest.cs +++ b/API.Tests/Parser/ParserTest.cs @@ -11,6 +11,7 @@ namespace API.Tests.Parser [InlineData("Beastars SP01", true)] [InlineData("Beastars Special 01", false)] [InlineData("Beastars Extra 01", false)] + [InlineData("Batman Beyond - Return of the Joker (2001) SP01", true)] public void HasSpecialTest(string input, bool expected) { Assert.Equal(expected, HasSpecialMarker(input)); @@ -35,14 +36,15 @@ namespace API.Tests.Parser } [Theory] - [InlineData("Hello_I_am_here", "Hello I am here")] - [InlineData("Hello_I_am_here ", "Hello I am here")] - [InlineData("[ReleaseGroup] The Title", "The Title")] - [InlineData("[ReleaseGroup]_The_Title", "The Title")] - [InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1", "Kasumi Otoko no Ko v1.1")] - public void CleanTitleTest(string input, string expected) + [InlineData("Hello_I_am_here", false, "Hello I am here")] + [InlineData("Hello_I_am_here ", false, "Hello I am here")] + [InlineData("[ReleaseGroup] The Title", false, "The Title")] + [InlineData("[ReleaseGroup]_The_Title", false, "The Title")] + [InlineData("[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1", false, "Kasumi Otoko no Ko v1.1")] + [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)); } diff --git a/API/Parser/Parser.cs b/API/Parser/Parser.cs index d1169d71c..bd462da28 100644 --- a/API/Parser/Parser.cs +++ b/API/Parser/Parser.cs @@ -25,32 +25,24 @@ namespace API.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant; public static readonly Regex FontSrcUrlRegex = new Regex(@"(src:url\(.{1})" + "([^\"']*)" + @"(.{1}\))", - MatchOptions, - RegexTimeout); + MatchOptions, RegexTimeout); public static readonly Regex CssImportUrlRegex = new Regex("(@import\\s[\"|'])(?[\\w\\d/\\._-]+)([\"|'];?)", - MatchOptions, - RegexTimeout); + MatchOptions, RegexTimeout); private static readonly string XmlRegexExtensions = @"\.xml"; private static readonly Regex ImageRegex = new Regex(ImageFileExtensions, - MatchOptions, - RegexTimeout); + MatchOptions, RegexTimeout); private static readonly Regex ArchiveFileRegex = new Regex(ArchiveFileExtensions, - MatchOptions, - RegexTimeout); + MatchOptions, RegexTimeout); private static readonly Regex XmlRegex = new Regex(XmlRegexExtensions, - MatchOptions, - RegexTimeout); + MatchOptions, RegexTimeout); private static readonly Regex BookFileRegex = new Regex(BookFileExtensions, - MatchOptions, - RegexTimeout); + MatchOptions, RegexTimeout); private static readonly Regex CoverImageRegex = new Regex(@"(?.*)(\b|_)v(?\d+-?\d+)( |_)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // NEEDLESS_Vol.4_-Simeon_6_v2[SugoiSugoi].rar new Regex( @"(?.*)(\b|_)(?!\[)(vol\.?)(?\d+(-\d+)?)(?!\])", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Historys Strongest Disciple Kenichi_v11_c90-98.zip or Dance in the Vampire Bund v16-17 new Regex( @"(?.*)(\b|_)(?!\[)v(?\d+(-\d+)?)(?!\])", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Kodomo no Jikan vol. 10, [dmntsf.net] One Piece - Digital Colored Comics Vol. 20.5-21.5 Ch. 177 new Regex( @"(?.*)(\b|_)(vol\.? ?)(?\d+(\.\d)?(-\d+)?(\.\d)?)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb) new Regex( @"(vol\.? ?)(?\d+(\.\d)?)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Tonikaku Cawaii [Volume 11].cbz new Regex( @"(volume )(?\d+(\.\d)?)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Tower Of God S01 014 (CBT) (digital).cbz new Regex( @"(?.*)(\b|_|)(S(?\d+))", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // vol_001-1.cbz for MangaPy default naming convention new Regex( @"(vol_)(?\d+(\.\d)?)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), }; private static readonly Regex[] MangaSeriesRegex = new[] @@ -102,13 +86,11 @@ namespace API.Parser // Grand Blue Dreaming - SP02 new Regex( @"(?.*)(\b|_|-|\s)(?:sp)\d", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // [SugoiSugoi]_NEEDLESS_Vol.2_-_Disk_The_Informant_5_[ENG].rar, Yuusha Ga Shinda! - Vol.tbd Chapter 27.001 V2 Infection ①.cbz new Regex( @"^(?.*)( |_)Vol\.?(\d+|tbd)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // 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 new Regex( @@ -123,23 +105,19 @@ namespace API.Parser // Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA], Black Bullet - v4 c17 [batoto] new Regex( @"(?.*)( - )(?:v|vo|c)\d", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Kedouin Makoto - Corpse Party Musume, Chapter 19 [Dametrans].zip new Regex( @"(?.*)(?:, Chapter )(?\d+)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Please Go Home, Akutsu-San! - Chapter 038.5 - Volume Announcement.cbz new Regex( @"(?.*)(\s|_|-)(?!Vol)(\s|_|-)(?:Chapter)(\s|_|-)(?\d+)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // [dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz new Regex( @"(?.*) (\b|_|-)(vol)\.?(\s|-|_)?\d+", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // [xPearse] Kyochuu Rettou Volume 1 [English] [Manga] [Volume Scans] new Regex( @"(?.*) (\b|_|-)(vol)(ume)", @@ -148,121 +126,98 @@ namespace API.Parser //Knights of Sidonia c000 (S2 LE BD Omake - BLAME!) [Habanero Scans] new Regex( @"(?.*)(\bc\d+\b)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), //Tonikaku Cawaii [Volume 11], Darling in the FranXX - Volume 01.cbz new Regex( @"(?.*)(?: _|-|\[|\()\s?vol(ume)?", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Momo The Blood Taker - Chapter 027 Violent Emotion.cbz, Grand Blue Dreaming - SP02 Extra (2019) (Digital) (danke-Empire).cbz new Regex( @"^(?(?!Vol).+?)(?:(ch(apter|\.)(\b|_|-|\s))|sp)\d", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Historys Strongest Disciple Kenichi_v11_c90-98.zip, Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb) new Regex( @"(?.*) (\b|_|-)(v|ch\.?|c)\d+", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), //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. new Regex( @"(?.*)(v|s)\d+(-\d+)?(_|\s)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), //[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip new Regex( @"(?.*)(v|s)\d+(-\d+)?", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz new Regex( @"(?.*) (?\d+) (?:\(\d{4}\)) ", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Goblin Slayer - Brand New Day 006.5 (2019) (Digital) (danke-Empire) new Regex( @"(?.*) (?\d+(?:.\d+|-\d+)?) \(\d{4}\)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Noblesse - Episode 429 (74 Pages).7z new Regex( @"(?.*)(\s|_)(?:Episode|Ep\.?)(\s|_)(?\d+(?:.\d+|-\d+)?)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Akame ga KILL! ZERO (2016-2019) (Digital) (LuCaZ) new Regex( @"(?.*)\(\d", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Tonikaku Kawaii (Ch 59-67) (Ongoing) new Regex( @"(?.*)(\s|_)\((c\s|ch\s|chapter\s)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Black Bullet (This is very loose, keep towards bottom) new Regex( @"(?.*)(_)(v|vo|c|volume)( |_)\d+", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // [Hidoi]_Amaenaideyo_MS_vol01_chp02.rar new Regex( @"(?.*)( |_)(vol\d+)?( |_)(?:Chp\.? ?\d+)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Mahoutsukai to Deshi no Futekisetsu na Kankei Chp. 1 new Regex( @"(?.*)( |_)(?:Chp.? ?\d+)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Chapter 01 new Regex( @"^(?!Vol)(?.*)( |_)Chapter( |_)(\d+)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Fullmetal Alchemist chapters 101-108.cbz new Regex( @"^(?!vol)(?.*)( |_)(chapters( |_)?)\d+-?\d*", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Umineko no Naku Koro ni - Episode 1 - Legend of the Golden Witch #1 new Regex( @"^(?!Vol\.?)(?.*)( |_|-)(?.*)ch\d+-?\d?", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Magi - Ch.252-005.cbz new Regex( @"(?.*)( ?- ?)Ch\.\d+-?\d*", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // [BAA]_Darker_than_Black_Omake-1.zip new Regex( @"^(?!Vol)(?.*)(-)\d+-?\d*", // This catches a lot of stuff ^(?!Vol)(?.*)( |_)(\d+) - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Kodoja #001 (March 2016) new Regex( @"(?.*)(\s|_|-)#", - MatchOptions, - RegexTimeout), + MatchOptions, 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) new Regex( @"^(?!Vol\.?)(?.+?)( |_|-)(?.*)( |_|-)(ch?)\d+", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), }; private static readonly Regex[] ComicSeriesRegex = new[] @@ -270,115 +225,79 @@ namespace API.Parser // Invincible Vol 01 Family matters (2005) (Digital) new Regex( @"(?.*)(\b|_)(vol\.?)( |_)(?\d+(-\d+)?)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), + // Batman Beyond 2.0 001 (2013) + new Regex( + @"^(?.+?\S\.\d) (?\d+)", + MatchOptions, RegexTimeout), // 04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS) new Regex( @"^(?\d+) (- |_)?(?.*(\d{4})?)( |_)(\(|\d+)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // 01 Spider-Man & Wolverine 01.cbr new Regex( @"^(?\d+) (?:- )?(?.*) (\d+)?", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Batman & Wildcat (1 of 3) new Regex( @"(?.*(\d{4})?)( |_)(?:\((?\d+) of \d+)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus) new Regex( @"^(?.*)(?: |_)v\d+", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Amazing Man Comics chapter 25 new Regex( @"^(?.*)(?: |_)c(hapter) \d+", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Amazing Man Comics issue #25 new Regex( @"^(?.*)(?: |_)i(ssue) #\d+", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Batman Wayne Family Adventures - Ep. 001 - Moving In new Regex( @"^(?.+?)(\s|_|-)?(?:Ep\.?)(\s|_|-)+\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) + MatchOptions, RegexTimeout), + // Batgirl Vol.2000 #57 (December, 2004) new Regex( - @"^(?.+?)(?: \d+)", - MatchOptions, - RegexTimeout), + @"^(?.+?)Vol\.?\s?#?(?:\d+)", + MatchOptions, RegexTimeout), // Batman & Robin the Teen Wonder #0 new Regex( @"^(?.*)(?: |_)#\d+", - MatchOptions, - RegexTimeout), + 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( + @"^(?.+?)(?: \d+)", + MatchOptions, RegexTimeout), // Scott Pilgrim 02 - Scott Pilgrim vs. The World (2005) new Regex( @"^(?.*)(?: |_)(?\d+)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // The First Asterix Frieze (WebP by Doc MaKS) new Regex( @"^(?.*)(?: |_)(?!\(\d{4}|\d{4}-\d{2}\))\(", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // spawn-123 (from https://github.com/Girbons/comics-downloader) new Regex( @"^(?.+?)-(?\d+)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // MUST BE LAST: Batman & Daredevil - King of New York new Regex( @"^(?.*)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), }; private static readonly Regex[] ComicVolumeRegex = new[] { - // // 04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS) - // new Regex( - // @"^(?\d+) (- |_)?(?.*(\d{4})?)( |_)(\(|\d+)", - // MatchOptions, - // RegexTimeout), - // // 01 Spider-Man & Wolverine 01.cbr - // new Regex( - // @"^(?\d+) (?:- )?(?.*) (\d+)?", - // MatchOptions, - // RegexTimeout), - // // Batman & Wildcat (1 of 3) - // new Regex( - // @"(?.*(\d{4})?)( |_)(?:\((?\d+) of \d+)", - // MatchOptions, - // RegexTimeout), // Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus) new Regex( @"^(?.*)(?: |_)v(?\d+)", - MatchOptions, - RegexTimeout), - // Scott Pilgrim 02 - Scott Pilgrim vs. The World (2005) - // BUG: Negative lookbehind has to be fixed width - // NOTE: The case this is built for does not make much sense. - // new Regex( - // @"^(?.+?)(?\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( - // @"^(?.+?)(?\d+))", - // MatchOptions, - // RegexTimeout), - // // Batman & Robin the Teen Wonder #0 - // new Regex( - // @"^(?.*)(?: |_)#(?\d+)", - // MatchOptions, - // RegexTimeout), + MatchOptions, RegexTimeout), + // Batgirl Vol.2000 #57 (December, 2004) + new Regex( + @"^(?.+?)(?:\s|_)vol\.?\s?(?\d+)", + MatchOptions, RegexTimeout), }; private static readonly Regex[] ComicChapterRegex = new[] @@ -386,61 +305,65 @@ namespace API.Parser // Batman & Wildcat (1 of 3) new Regex( @"(?.*(\d{4})?)( |_)(?:\((?\d+) of \d+)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Batman Beyond 04 (of 6) (1999) new Regex( @"(?.+?)(?\d+)(\s|_|-)?\(of", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), + // Batman Beyond 2.0 001 (2013) + new Regex( + @"^(?.+?\S\.\d) (?\d+)", + MatchOptions, RegexTimeout), // Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus) new Regex( @"^(?.+?)(?: |_)v(?\d+)(?: |_)(c? ?)(?(\d+(\.\d)?)-?(\d+(\.\d)?)?)(c? ?)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), + // Batman & Robin the Teen Wonder #0 + new Regex( + @"^(?.+?)(?:\s|_)#(?\d+)", + MatchOptions, RegexTimeout), // Invincible 070.5 - Invincible Returns 1 (2010) (digital) (Minutemen-InnerDemons).cbr new Regex( @"^(?.+?)(?: |_)(c? ?)(?(\d+(\.\d)?)-?(\d+(\.\d)?)?)(c? ?)-", + MatchOptions, RegexTimeout), + // Batgirl Vol.2000 #57 (December, 2004) + new Regex( + @"^(?.+?)(?:vol\.?\d+)\s#(?\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( @"^(?.+?)(?: (?\d+))", - MatchOptions, - RegexTimeout), - // Batman & Robin the Teen Wonder #0 - new Regex( - @"^(?.+?)(?:\s|_)#(?\d+)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), + // Saga 001 (2012) (Digital) (Empire-Zone) new Regex( @"(?.+?)(?: |_)(c? ?)(?(\d+(\.\d)?)-?(\d+(\.\d)?)?)\s\(\d{4}", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Amazing Man Comics chapter 25 new Regex( @"^(?!Vol)(?.+?)( |_)c(hapter)( |_)(?\d*)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Amazing Man Comics issue #25 new Regex( @"^(?!Vol)(?.+?)( |_)i(ssue)( |_) #(?\d*)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // spawn-123 (from https://github.com/Girbons/comics-downloader ) new Regex( @"^(?.+?)-(?\d+)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), + // Cyberpunk 2077 - Your Voice 01 + // new Regex( + // @"^(?.+?\s?-\s?(?:.+?))(?(\d+(\.\d)?)-?(\d+(\.\d)?)?)$", + // MatchOptions, + // RegexTimeout), }; private static readonly Regex[] ReleaseGroupRegex = new[] { // [TrinityBAKumA Finella&anon], [BAA]_, [SlowManga&OverloadScans], [batoto] new Regex(@"(?:\[(?(?!\s).+?(?(?!\s).+?(?(\d+(\.\d)?)-?(\d+(\.\d)?)?)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip new Regex( @"v\d+\.(?\d+(?:.\d+|-\d+)?)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Umineko no Naku Koro ni - Episode 3 - Banquet of the Golden Witch #02.cbz (Rare case, if causes issue remove) new Regex( @"^(?.*)(?: |_)#(?\d+)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Green Worldz - Chapter 027, Kimi no Koto ga Daidaidaidaidaisuki na 100-nin no Kanojo Chapter 11-10 new Regex( @"^(?!Vol)(?.*)\s?(?\d+(?:\.?[\d-]+)?)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz, Hinowa ga CRUSH! 018.5 (2019) (Digital) (LuCaZ).cbz new Regex( @"^(?!Vol)(?.+?)(?\d+(?:.\d+|-\d+)?)(?:\s\(\d{4}\))?(\b|_|-)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Tower Of God S01 014 (CBT) (digital).cbz new Regex( @"(?.*)\sS(?\d+)\s(?\d+(?:.\d+|-\d+)?)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Beelzebub_01_[Noodles].zip, Beelzebub_153b_RHS.zip new Regex( @"^((?!v|vo|vol|Volume).)*(\s|_)(?\.?\d+(?:.\d+|-\d+)?)(?b)?(\s|_|\[|\()", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Yumekui-Merry_DKThias_Chapter21.zip new Regex( @"Chapter(?\d+(-\d+)?)", //(?:.\d+|-\d+)? - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // [Hidoi]_Amaenaideyo_MS_vol01_chp02.rar new Regex( @"(?.*)(\s|_)(vol\d+)?(\s|_)Chp\.? ?(?\d+)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Vol 1 Chapter 2 new Regex( @"(?((vol|volume|v))?(\s|_)?\.?\d+)(\s|_)(Chp|Chapter)\.?(\s|_)?(?\d+)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), }; private static readonly Regex[] MangaEditionRegex = { // Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz new Regex( @"(?({|\(|\[).* Edition(}|\)|\]))", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz new Regex( @"(\b|_)(?Omnibus(( |_)?Edition)?)(\b|_)?", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // To Love Ru v01 Uncensored (Ch.001-007) new Regex( @"(\b|_)(?Uncensored)(\b|_)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // AKIRA - c003 (v01) [Full Color] [Darkhorse].cbz new Regex( @"(\b|_)(?Full(?: |_)Color)(\b|_)?", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), }; private static readonly Regex[] CleanupRegex = @@ -528,18 +437,15 @@ namespace API.Parser // (), {}, [] new Regex( @"(?(\{\}|\[\]|\(\)))", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // (Complete) new Regex( @"(?(\{Complete\}|\[Complete\]|\(Complete\)))", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), // Anything in parenthesis new Regex( @"\(.*\)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), }; 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. new Regex( @"(?Specials?|OneShot|One\-Shot|Omake|Extra( Chapter)?|Art Collection|Side( |_)Stories|Bonus)", - MatchOptions, - RegexTimeout), + MatchOptions, RegexTimeout), + }; + + private static readonly Regex[] ComicSpecialRegex = + { + // All Keywords, does not account for checking if contains volume/chapter identification. Parser.Parse() will handle. + new Regex( + @"(?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. private static readonly Regex SpecialMarkerRegex = new Regex( @"(?SP\d+)", - MatchOptions, - RegexTimeout + MatchOptions, RegexTimeout ); @@ -569,7 +481,7 @@ namespace API.Parser /// or null if Series was empty public static ParserInfo Parse(string filePath, string rootPath, LibraryType type = LibraryType.Manga) { - var fileName = Path.GetFileName(filePath); + var fileName = Path.GetFileNameWithoutExtension(filePath); ParserInfo ret; if (IsEpub(filePath)) @@ -579,7 +491,7 @@ namespace API.Parser Chapters = ParseChapter(fileName) ?? ParseComicChapter(fileName), Series = ParseSeries(fileName) ?? ParseComicSeries(fileName), Volumes = ParseVolume(fileName) ?? ParseComicVolume(fileName), - Filename = fileName, + Filename = Path.GetFileName(filePath), Format = ParseFormat(filePath), FullFilePath = filePath }; @@ -591,14 +503,14 @@ namespace API.Parser Chapters = type == LibraryType.Manga ? ParseChapter(fileName) : ParseComicChapter(fileName), Series = type == LibraryType.Manga ? ParseSeries(fileName) : ParseComicSeries(fileName), Volumes = type == LibraryType.Manga ? ParseVolume(fileName) : ParseComicVolume(fileName), - Filename = fileName, + Filename = Path.GetFileName(filePath), Format = ParseFormat(filePath), Title = Path.GetFileNameWithoutExtension(fileName), FullFilePath = filePath }; } - if (IsImage(filePath) && IsCoverImage(fileName)) return null; + if (IsImage(filePath) && IsCoverImage(filePath)) return null; if (IsImage(filePath)) { @@ -617,7 +529,7 @@ namespace API.Parser var edition = ParseEdition(fileName); 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; } @@ -642,11 +554,11 @@ namespace API.Parser 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 - 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); } @@ -690,7 +602,7 @@ namespace API.Parser if ((string.IsNullOrEmpty(series) && i == fallbackFolders.Count - 1)) { - ret.Series = CleanTitle(folder); + ret.Series = CleanTitle(folder, type is LibraryType.Comic); break; } @@ -767,6 +679,23 @@ namespace API.Parser 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) { foreach (var regex in MangaSeriesRegex) @@ -792,7 +721,7 @@ namespace API.Parser { 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) { - 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) + { + 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); foreach (Match match in matches) @@ -932,9 +879,9 @@ namespace API.Parser 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); foreach (Match match in matches) @@ -958,14 +905,16 @@ namespace API.Parser /// /// /// + /// /// - public static string CleanTitle(string title) + public static string CleanTitle(string title, bool isComic = false) { title = RemoveReleaseGroup(title); title = RemoveEditionTagHolders(title); - title = RemoveSpecialTags(title); + title = isComic ? RemoveComicSpecialTags(title) : RemoveMangaSpecialTags(title); + title = title.Replace("_", " ").Trim(); if (title.EndsWith("-") || title.EndsWith(",")) diff --git a/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts b/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts index 33aff1719..8312c7325 100644 --- a/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts +++ b/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts @@ -22,6 +22,8 @@ import { MemberService } from 'src/app/_services/member.service'; import { ReadingDirection } from 'src/app/_models/preferences/reading-direction'; import { ScrollService } from 'src/app/scroll.service'; 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 { @@ -169,6 +171,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { * Last seen progress part path */ lastSeenScrollPartPath: string = ''; + /** + * Library Type used for rendering chapter or issue + */ + libraryType: LibraryType = LibraryType.Book; /** * 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 renderer: Renderer2, private navService: NavService, private toastr: ToastrService, 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.darkModeStyleElem = this.renderer.createElement('style'); @@ -421,6 +427,11 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.continuousChaptersStack.push(this.chapterId); + + this.libraryService.getLibraryType(this.libraryId).pipe(take(1)).subscribe(type => { + this.libraryType = type; + }); + 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); window.history.replaceState({}, '', newRoute); this.init(); - this.toastr.info(direction + ' chapter loaded', '', {timeOut: 3000}); + this.toastr.info(direction + ' ' + this.utilityService.formatChapterName(this.libraryType).toLowerCase() + ' loaded', '', {timeOut: 3000}); } else { // 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; if (direction === 'Prev') { this.prevPageDisabled = true; diff --git a/UI/Web/src/app/cards/_modals/card-details-modal/card-details-modal.component.html b/UI/Web/src/app/cards/_modals/card-details-modal/card-details-modal.component.html index 85e17f125..80726b878 100644 --- a/UI/Web/src/app/cards/_modals/card-details-modal/card-details-modal.component.html +++ b/UI/Web/src/app/cards/_modals/card-details-modal/card-details-modal.component.html @@ -1,7 +1,10 @@
-

Chapters

+

{{utilityService.formatChapterName(libraryType) + 's'}}

  • - +
    - -   - Chapter {{formatChapterNumber(chapter)}} + +   + {{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}} + {{chapter.pagesRead}} / {{chapter.pages}} UNREAD diff --git a/UI/Web/src/app/cards/_modals/card-details-modal/card-details-modal.component.ts b/UI/Web/src/app/cards/_modals/card-details-modal/card-details-modal.component.ts index 6cd02f5c8..3f15edd65 100644 --- a/UI/Web/src/app/cards/_modals/card-details-modal/card-details-modal.component.ts +++ b/UI/Web/src/app/cards/_modals/card-details-modal/card-details-modal.component.ts @@ -13,6 +13,8 @@ import { ActionService } from 'src/app/_services/action.service'; import { ImageService } from 'src/app/_services/image.service'; import { UploadService } from 'src/app/_services/upload.service'; 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; actions: ActionItem[] = []; chapterActions: ActionItem[] = []; + libraryType: LibraryType = LibraryType.Manga; + get LibraryType(): typeof LibraryType { + return LibraryType; + } constructor(private modalService: NgbModal, public modal: NgbActiveModal, public utilityService: UtilityService, public imageService: ImageService, private uploadService: UploadService, private toastr: ToastrService, private accountService: AccountService, private actionFactoryService: ActionFactoryService, - private actionService: ActionService, private router: Router) { } + private actionService: ActionService, private router: Router, private libraryService: LibraryService) { } ngOnInit(): void { 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); if (this.isChapter) { @@ -94,7 +104,7 @@ export class CardDetailsModalComponent implements OnInit { const chapter = this.utilityService.asChapter(this.data) chapter.coverImage = this.imageService.getChapterCoverImage(chapter.id); 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 { const volume = this.utilityService.asVolume(this.data); const chapters = volume.chapters; diff --git a/UI/Web/src/app/manga-reader/manga-reader.component.ts b/UI/Web/src/app/manga-reader/manga-reader.component.ts index 843006726..ec9c0ba90 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.ts +++ b/UI/Web/src/app/manga-reader/manga-reader.component.ts @@ -12,7 +12,7 @@ import { ScalingOption } from '../_models/preferences/scaling-option'; import { PageSplitOption } from '../_models/preferences/page-split-option'; import { forkJoin, ReplaySubject, Subject } from 'rxjs'; 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 { MemberService } from '../_services/member.service'; 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 { READER_MODE } from '../_models/preferences/reader-mode'; import { MangaFormat } from '../_models/manga-format'; +import { LibraryService } from '../_services/library.service'; +import { LibraryType } from '../_models/library'; 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. */ firstPageRendered: boolean = false; + /** + * Library Type used for rendering chapter or issue + */ + libraryType: LibraryType = LibraryType.Manga; private readonly onDestroy = new Subject(); @@ -260,7 +266,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService, public readerService: ReaderService, private location: Location, 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(); } @@ -400,7 +407,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { forkJoin({ progress: this.readerService.getProgress(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 => { 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. 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) 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; if (chapterInfo.chapterTitle.length > 0) { this.title += ' - ' + chapterInfo.chapterTitle; @@ -489,12 +501,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { if (chapterInfo.isSpecial && chapterInfo.volumeNumber === '0') { this.subtitle = chapterInfo.fileName; } else if (!chapterInfo.isSpecial && chapterInfo.volumeNumber === '0') { - this.subtitle = 'Chapter ' + chapterInfo.chapterNumber; + this.subtitle = this.utilityService.formatChapterName(type, true, true) + chapterInfo.chapterNumber; } else { this.subtitle = 'Volume ' + chapterInfo.volumeNumber; 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); window.history.replaceState({}, '', newRoute); this.init(); - this.toastr.info(direction + ' chapter loaded', '', {timeOut: 3000}); + this.toastr.info(direction + ' ' + this.utilityService.formatChapterName(this.libraryType).toLowerCase() + ' loaded', '', {timeOut: 3000}); } else { // 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; if (direction === 'Prev') { this.prevPageDisabled = true; diff --git a/UI/Web/src/app/reading-list/reading-list-detail/reading-list-detail.component.ts b/UI/Web/src/app/reading-list/reading-list-detail/reading-list-detail.component.ts index 5c1728157..fb4c3a709 100644 --- a/UI/Web/src/app/reading-list/reading-list-detail/reading-list-detail.component.ts +++ b/UI/Web/src/app/reading-list/reading-list-detail/reading-list-detail.component.ts @@ -4,6 +4,7 @@ import { ToastrService } from 'ngx-toastr'; import { take } from 'rxjs/operators'; import { ConfirmService } from 'src/app/shared/confirm.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 { ReadingList, ReadingListItem } from 'src/app/_models/reading-list'; 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 { ReadingListService } from 'src/app/_services/reading-list.service'; import { IndexUpdateEvent, ItemRemoveEvent } from '../dragable-ordered-list/dragable-ordered-list.component'; +import { LibraryService } from '../../_services/library.service'; +import { forkJoin } from 'rxjs'; @Component({ selector: 'app-reading-list-detail', @@ -19,7 +22,6 @@ import { IndexUpdateEvent, ItemRemoveEvent } from '../dragable-ordered-list/drag styleUrls: ['./reading-list-detail.component.scss'] }) export class ReadingListDetailComponent implements OnInit { - items: Array = []; listId!: number; readingList!: ReadingList; @@ -32,6 +34,7 @@ export class ReadingListDetailComponent implements OnInit { hasDownloadingRole: boolean = false; downloadInProgress: boolean = false; + libraryTypes: {[key: number]: LibraryType} = {}; get MangaFormat(): typeof MangaFormat { return MangaFormat; @@ -39,7 +42,8 @@ export class ReadingListDetailComponent implements OnInit { constructor(private route: ActivatedRoute, private router: Router, private readingListService: ReadingListService, 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 { const listId = this.route.snapshot.paramMap.get('id'); @@ -51,7 +55,21 @@ export class ReadingListDetailComponent implements OnInit { 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) { // The list doesn't exist this.toastr.error('This list doesn\'t exist.'); @@ -81,7 +99,6 @@ export class ReadingListDetailComponent implements OnInit { } performAction(action: ActionItem) { - // 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') { action.callback(action.action, this.readingList); } @@ -119,7 +136,7 @@ export class ReadingListDetailComponent implements OnInit { 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) { diff --git a/UI/Web/src/app/series-detail/series-detail.component.html b/UI/Web/src/app/series-detail/series-detail.component.html index 541ff5b88..a80694687 100644 --- a/UI/Web/src/app/series-detail/series-detail.component.html +++ b/UI/Web/src/app/series-detail/series-detail.component.html @@ -112,7 +112,7 @@
  • - Volumes/Chapters + {{utilityService.formatChapterName(libraryType) + 's'}}
    @@ -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">
    -
    diff --git a/UI/Web/src/app/shared/_services/utility.service.ts b/UI/Web/src/app/shared/_services/utility.service.ts index 23fb459f8..95751885e 100644 --- a/UI/Web/src/app/shared/_services/utility.service.ts +++ b/UI/Web/src/app/shared/_services/utility.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core'; import { Chapter } from 'src/app/_models/chapter'; +import { LibraryType } from 'src/app/_models/library'; import { MangaFormat } from 'src/app/_models/manga-format'; import { Series } from 'src/app/_models/series'; import { Volume } from 'src/app/_models/volume'; @@ -56,6 +57,27 @@ export class UtilityService { 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) { let cleaned = title.replace(/_/g, ' ').replace(/SP\d+/g, '').trim(); cleaned = cleaned.substring(0, cleaned.lastIndexOf('.'));