From 93df0def48ba2e031b77408414fdf8c8a7307177 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Mon, 8 Jan 2024 13:58:08 -0600 Subject: [PATCH 1/6] Started working on the magazine library type. Taking a break as I need some more community feedback. --- API.Tests/Parser/MagazineParserTests.cs | 20 + API/Entities/Enums/LibraryType.cs | 5 + .../Tasks/Scanner/Parser/DefaultParser.cs | 22 ++ API/Services/Tasks/Scanner/Parser/Parser.cs | 371 ++++++++++-------- API/Services/Tasks/Scanner/ProcessSeries.cs | 1 + UI/Web/src/app/_models/library/library.ts | 3 +- UI/Web/src/app/_pipes/library-type.pipe.ts | 2 + .../app/shared/_services/utility.service.ts | 17 +- .../side-nav/side-nav.component.ts | 2 + .../library-settings-modal.component.ts | 6 + UI/Web/src/assets/langs/en.json | 3 +- openapi.json | 30 +- 12 files changed, 306 insertions(+), 176 deletions(-) create mode 100644 API.Tests/Parser/MagazineParserTests.cs diff --git a/API.Tests/Parser/MagazineParserTests.cs b/API.Tests/Parser/MagazineParserTests.cs new file mode 100644 index 000000000..417d6e1b9 --- /dev/null +++ b/API.Tests/Parser/MagazineParserTests.cs @@ -0,0 +1,20 @@ +using Xunit; + +namespace API.Tests.Parser; + +public class MagazineParserTests +{ + [Theory] + [InlineData("3D World - 2018 UK", "3D World")] + public void ParseSeriesTest(string filename, string expected) + { + Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseMagazineSeries(filename)); + } + + // [Theory] + // [InlineData("Harrison, Kim - Dates from Hell - Hollows Vol 2.5.epub", "2.5")] + // public void ParseVolumeTest(string filename, string expected) + // { + // Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseMagazineVolume(filename)); + // } +} diff --git a/API/Entities/Enums/LibraryType.cs b/API/Entities/Enums/LibraryType.cs index 038ce7172..7206d423a 100644 --- a/API/Entities/Enums/LibraryType.cs +++ b/API/Entities/Enums/LibraryType.cs @@ -24,4 +24,9 @@ public enum LibraryType /// [Description("Image")] Image = 3, + /// + /// Uses Magazine regex and is restricted to PDF and Archive by default + /// + [Description("Magazine")] + Magazine = 4 } diff --git a/API/Services/Tasks/Scanner/Parser/DefaultParser.cs b/API/Services/Tasks/Scanner/Parser/DefaultParser.cs index 742ed50d1..ce41a593c 100644 --- a/API/Services/Tasks/Scanner/Parser/DefaultParser.cs +++ b/API/Services/Tasks/Scanner/Parser/DefaultParser.cs @@ -52,6 +52,11 @@ public class DefaultParser : IDefaultParser return ParseImage(filePath, rootPath, ret); } + if (type == LibraryType.Magazine) + { + return ParseMagazine(filePath, rootPath, ret); + } + // This will be called if the epub is already parsed once then we call and merge the information, if the if (Parser.IsEpub(filePath)) @@ -115,6 +120,23 @@ public class DefaultParser : IDefaultParser return ret.Series == string.Empty ? null : ret; } + private ParserInfo ParseMagazine(string filePath, string rootPath, ParserInfo ret) + { + // Try to parse Series from the filename + var libraryPath = _directoryService.FileSystem.DirectoryInfo.New(rootPath).Parent?.FullName ?? rootPath; + ret.Series = Parser.ParseMagazineSeries(filePath); + if (string.IsNullOrEmpty(ret.Series)) + { + // Fallback to the parent folder. We can also likely grab Volume (year) from here + var folders = _directoryService.GetFoldersTillRoot(libraryPath, filePath); + foreach (var folder in folders) + { + + } + } + return ret; + } + private ParserInfo ParseImage(string filePath, string rootPath, ParserInfo ret) { ret.Volumes = Parser.DefaultVolume; diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index 0b71ec67b..214c2009c 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -102,79 +102,7 @@ public static class Parser private static readonly Regex SpecialTokenRegex = new Regex(@"SP\d+", MatchOptions, RegexTimeout); - - private static readonly Regex[] MangaVolumeRegex = new[] - { - // Dance in the Vampire Bund v16-17 - new Regex( - @"(?.*)(\b|_)v(?\d+-?\d+)( |_)", - MatchOptions, RegexTimeout), - // Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.31 Omake - new Regex( - @"^(?.+?)(\s*Chapter\s*\d+)?(\s|_|\-\s)+(Vol(ume)?\.?(\s|_)?)(?\d+(\.\d+)?)(.+?|$)", - MatchOptions, RegexTimeout), - // Historys Strongest Disciple Kenichi_v11_c90-98.zip or Dance in the Vampire Bund v16-17 - new Regex( - @"(?.*)(\b|_)(?!\[)v(?" + NumberRange + @")(?!\])", - 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), - // Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb) - new Regex( - @"(vol\.? ?)(?\d+(\.\d)?)", - MatchOptions, RegexTimeout), - // Tonikaku Cawaii [Volume 11].cbz - new Regex( - @"(volume )(?\d+(\.\d)?)", - MatchOptions, RegexTimeout), - // Tower Of God S01 014 (CBT) (digital).cbz - new Regex( - @"(?.*)(\b|_|)(S(?\d+))", - MatchOptions, RegexTimeout), - // vol_001-1.cbz for MangaPy default naming convention - new Regex( - @"(vol_)(?\d+(\.\d)?)", - MatchOptions, RegexTimeout), - - // Chinese Volume: 第n卷 -> Volume n, 第n册 -> Volume n, 幽游白书完全版 第03卷 天下 or 阿衰online 第1册 - new Regex( - @"第(?\d+)(卷|册)", - MatchOptions, RegexTimeout), - // Chinese Volume: 卷n -> Volume n, 册n -> Volume n - new Regex( - @"(卷|册)(?\d+)", - MatchOptions, RegexTimeout), - // Korean Volume: 제n화|권|회|장 -> Volume n, n화|권|회|장 -> Volume n, 63권#200.zip -> Volume 63 (no chapter, #200 is just files inside) - new Regex( - @"제?(?\d+(\.\d)?)(권|회|화|장)", - MatchOptions, RegexTimeout), - // Korean Season: 시즌n -> Season n, - new Regex( - @"시즌(?\d+\-?\d+)", - MatchOptions, RegexTimeout), - // Korean Season: 시즌n -> Season n, n시즌 -> season n - new Regex( - @"(?\d+(\-|~)?\d+?)시즌", - MatchOptions, RegexTimeout), - // Korean Season: 시즌n -> Season n, n시즌 -> season n - new Regex( - @"시즌(?\d+(\-|~)?\d+?)", - MatchOptions, RegexTimeout), - // Japanese Volume: n巻 -> Volume n - new Regex( - @"(?\d+(?:(\-)\d+)?)巻", - MatchOptions, RegexTimeout), - // Russian Volume: Том n -> Volume n, Тома n -> Volume - new Regex( - @"Том(а?)(\.?)(\s|_)?(?\d+(?:(\-)\d+)?)", - MatchOptions, RegexTimeout), - // Russian Volume: n Том -> Volume n - new Regex( - @"(\s|_)?(?\d+(?:(\-)\d+)?)(\s|_)Том(а?)", - MatchOptions, RegexTimeout), - }; + #region Manga private static readonly Regex[] MangaSeriesRegex = new[] { @@ -349,7 +277,158 @@ public static class Parser MatchOptions, RegexTimeout), }; + private static readonly Regex[] MangaVolumeRegex = new[] + { + // Dance in the Vampire Bund v16-17 + new Regex( + @"(?.*)(\b|_)v(?\d+-?\d+)( |_)", + MatchOptions, RegexTimeout), + // Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.31 Omake + new Regex( + @"^(?.+?)(\s*Chapter\s*\d+)?(\s|_|\-\s)+(Vol(ume)?\.?(\s|_)?)(?\d+(\.\d+)?)(.+?|$)", + MatchOptions, RegexTimeout), + // Historys Strongest Disciple Kenichi_v11_c90-98.zip or Dance in the Vampire Bund v16-17 + new Regex( + @"(?.*)(\b|_)(?!\[)v(?" + NumberRange + @")(?!\])", + 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), + // Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb) + new Regex( + @"(vol\.? ?)(?\d+(\.\d)?)", + MatchOptions, RegexTimeout), + // Tonikaku Cawaii [Volume 11].cbz + new Regex( + @"(volume )(?\d+(\.\d)?)", + MatchOptions, RegexTimeout), + // Tower Of God S01 014 (CBT) (digital).cbz + new Regex( + @"(?.*)(\b|_|)(S(?\d+))", + MatchOptions, RegexTimeout), + // vol_001-1.cbz for MangaPy default naming convention + new Regex( + @"(vol_)(?\d+(\.\d)?)", + MatchOptions, RegexTimeout), + // Chinese Volume: 第n卷 -> Volume n, 第n册 -> Volume n, 幽游白书完全版 第03卷 天下 or 阿衰online 第1册 + new Regex( + @"第(?\d+)(卷|册)", + MatchOptions, RegexTimeout), + // Chinese Volume: 卷n -> Volume n, 册n -> Volume n + new Regex( + @"(卷|册)(?\d+)", + MatchOptions, RegexTimeout), + // Korean Volume: 제n화|권|회|장 -> Volume n, n화|권|회|장 -> Volume n, 63권#200.zip -> Volume 63 (no chapter, #200 is just files inside) + new Regex( + @"제?(?\d+(\.\d)?)(권|회|화|장)", + MatchOptions, RegexTimeout), + // Korean Season: 시즌n -> Season n, + new Regex( + @"시즌(?\d+\-?\d+)", + MatchOptions, RegexTimeout), + // Korean Season: 시즌n -> Season n, n시즌 -> season n + new Regex( + @"(?\d+(\-|~)?\d+?)시즌", + MatchOptions, RegexTimeout), + // Korean Season: 시즌n -> Season n, n시즌 -> season n + new Regex( + @"시즌(?\d+(\-|~)?\d+?)", + MatchOptions, RegexTimeout), + // Japanese Volume: n巻 -> Volume n + new Regex( + @"(?\d+(?:(\-)\d+)?)巻", + MatchOptions, RegexTimeout), + // Russian Volume: Том n -> Volume n, Тома n -> Volume + new Regex( + @"Том(а?)(\.?)(\s|_)?(?\d+(?:(\-)\d+)?)", + MatchOptions, RegexTimeout), + // Russian Volume: n Том -> Volume n + new Regex( + @"(\s|_)?(?\d+(?:(\-)\d+)?)(\s|_)Том(а?)", + MatchOptions, RegexTimeout), + }; + private static readonly Regex[] MangaChapterRegex = new[] + { + // Historys Strongest Disciple Kenichi_v11_c90-98.zip, ...c90.5-100.5 + new Regex( + @"(\b|_)(c|ch)(\.?\s?)(?(\d+(\.\d)?)-?(\d+(\.\d)?)?)", + MatchOptions, RegexTimeout), + // [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip + new Regex( + @"v\d+\.(\s|_)(?\d+(?:.\d+|-\d+)?)", + 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), + // 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), + // Russian Chapter: Главы n -> Chapter n + new Regex( + @"(Глава|глава|Главы|Глава)(\.?)(\s|_)?(?\d+(?:.\d+|-\d+)?)", + MatchOptions, RegexTimeout), + + // Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz, Hinowa ga CRUSH! 018.5 (2019) (Digital) (LuCaZ).cbz + new Regex( + @"^(?.+?)(?\d+(?:\.\d+|-\d+)?)(?:\s\(\d{4}\))?(\b|_|-)", + MatchOptions, RegexTimeout), + // Tower Of God S01 014 (CBT) (digital).cbz + new Regex( + @"(?.*)\sS(?\d+)\s(?\d+(?:.\d+|-\d+)?)", + MatchOptions, RegexTimeout), + // Beelzebub_01_[Noodles].zip, Beelzebub_153b_RHS.zip + new Regex( + @"^((?!v|vo|vol|Volume).)*(\s|_)(?\.?\d+(?:.\d+|-\d+)?)(?b)?(\s|_|\[|\()", + MatchOptions, RegexTimeout), + // Yumekui-Merry_DKThias_Chapter21.zip + new Regex( + @"Chapter(?\d+(-\d+)?)", //(?:.\d+|-\d+)? + MatchOptions, RegexTimeout), + // [Hidoi]_Amaenaideyo_MS_vol01_chp02.rar + new Regex( + @"(?.*)(\s|_)(vol\d+)?(\s|_)Chp\.? ?(?\d+)", + MatchOptions, RegexTimeout), + // Vol 1 Chapter 2 + new Regex( + @"(?((vol|volume|v))?(\s|_)?\.?\d+)(\s|_)(Chp|Chapter)\.?(\s|_)?(?\d+)", + MatchOptions, RegexTimeout), + // Chinese Chapter: 第n话 -> Chapter n, 【TFO汉化&Petit汉化】迷你偶像漫画第25话 + new Regex( + @"第(?\d+)话", + MatchOptions, RegexTimeout), + // Korean Chapter: 제n화 -> Chapter n, 가디언즈 오브 갤럭시 죽음의 보석.E0008.7화#44 + new Regex( + @"제?(?\d+\.?\d+)(회|화|장)", + MatchOptions, RegexTimeout), + // Korean Chapter: 第10話 -> Chapter n, [ハレム]ナナとカオル ~高校生のSMごっこ~ 第1話 + new Regex( + @"第?(?\d+(?:\.\d+|-\d+)?)話", + MatchOptions, RegexTimeout), + // Russian Chapter: n Главa -> Chapter n + new Regex( + @"(?!Том)(?\d+(?:\.\d+|-\d+)?)(\s|_)(Глава|глава|Главы|Глава)", + MatchOptions, RegexTimeout), + }; + private static readonly Regex MangaEditionRegex = new Regex( + // Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz + // To Love Ru v01 Uncensored (Ch.001-007) + @"\b(?:Omnibus(?:\s?Edition)?|Uncensored)\b", + MatchOptions, RegexTimeout + ); + + private static readonly Regex MangaSpecialRegex = new Regex( + // All Keywords, does not account for checking if contains volume/chapter identification. Parser.Parse() will handle. + $@"\b(?:{CommonSpecial}|Omake)\b", + MatchOptions, RegexTimeout + ); + + #endregion + + #region Comic private static readonly Regex[] ComicSeriesRegex = new[] { // Russian Volume: Том n -> Volume n, Тома n -> Volume @@ -539,77 +618,44 @@ public static class Parser MatchOptions, RegexTimeout), }; - private static readonly Regex[] MangaChapterRegex = new[] - { - // Historys Strongest Disciple Kenichi_v11_c90-98.zip, ...c90.5-100.5 - new Regex( - @"(\b|_)(c|ch)(\.?\s?)(?(\d+(\.\d)?)-?(\d+(\.\d)?)?)", - MatchOptions, RegexTimeout), - // [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip - new Regex( - @"v\d+\.(\s|_)(?\d+(?:.\d+|-\d+)?)", - 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), - // 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), - // Russian Chapter: Главы n -> Chapter n - new Regex( - @"(Глава|глава|Главы|Глава)(\.?)(\s|_)?(?\d+(?:.\d+|-\d+)?)", - MatchOptions, RegexTimeout), + private static readonly Regex ComicSpecialRegex = new Regex( + // All Keywords, does not account for checking if contains volume/chapter identification. Parser.Parse() will handle. + $@"\b(?:{CommonSpecial}|\d.+?(\W|-|^)Annual|Annual(\W|-|$)|Book \d.+?|Compendium(\W|-|$|\s.+?)|Omnibus(\W|-|$|\s.+?)|FCBD \d.+?|Absolute(\W|-|$|\s.+?)|Preview(\W|-|$|\s.+?)|Hors[ -]S[ée]rie|TPB|HS|THS)\b", + MatchOptions, RegexTimeout + ); - // Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz, Hinowa ga CRUSH! 018.5 (2019) (Digital) (LuCaZ).cbz + private static readonly Regex EuropeanComicRegex = new Regex( + // All Keywords, does not account for checking if contains volume/chapter identification. Parser.Parse() will handle. + @"\b(?:Bd[-\s]Fr)\b", + MatchOptions, RegexTimeout + ); + + #endregion + + #region Magazine + + private static readonly Regex[] MagazineSeriesRegex = new[] + { + // 3D World - 2018 UK new Regex( - @"^(?.+?)(?\d+(?:\.\d+|-\d+)?)(?:\s\(\d{4}\))?(\b|_|-)", + @"(?.+?)(\b|_|\s)?-(\b|_|\s)(?\d{4}).+", MatchOptions, RegexTimeout), - // Tower Of God S01 014 (CBT) (digital).cbz + // AIR International - April 2018 UK + // The New Yorker - April 2, 2018 USA + // AIR International Magazine 2006 + // AIR International Vol. 14 No. 3 (ISSN 1011-3250) + }; + + private static readonly Regex[] MagazineVolumeRegex = new[] + { + // Batman & Wildcat (1 of 3) new Regex( - @"(?.*)\sS(?\d+)\s(?\d+(?:.\d+|-\d+)?)", - MatchOptions, RegexTimeout), - // Beelzebub_01_[Noodles].zip, Beelzebub_153b_RHS.zip - new Regex( - @"^((?!v|vo|vol|Volume).)*(\s|_)(?\.?\d+(?:.\d+|-\d+)?)(?b)?(\s|_|\[|\()", - MatchOptions, RegexTimeout), - // Yumekui-Merry_DKThias_Chapter21.zip - new Regex( - @"Chapter(?\d+(-\d+)?)", //(?:.\d+|-\d+)? - MatchOptions, RegexTimeout), - // [Hidoi]_Amaenaideyo_MS_vol01_chp02.rar - new Regex( - @"(?.*)(\s|_)(vol\d+)?(\s|_)Chp\.? ?(?\d+)", - MatchOptions, RegexTimeout), - // Vol 1 Chapter 2 - new Regex( - @"(?((vol|volume|v))?(\s|_)?\.?\d+)(\s|_)(Chp|Chapter)\.?(\s|_)?(?\d+)", - MatchOptions, RegexTimeout), - // Chinese Chapter: 第n话 -> Chapter n, 【TFO汉化&Petit汉化】迷你偶像漫画第25话 - new Regex( - @"第(?\d+)话", - MatchOptions, RegexTimeout), - // Korean Chapter: 제n화 -> Chapter n, 가디언즈 오브 갤럭시 죽음의 보석.E0008.7화#44 - new Regex( - @"제?(?\d+\.?\d+)(회|화|장)", - MatchOptions, RegexTimeout), - // Korean Chapter: 第10話 -> Chapter n, [ハレム]ナナとカオル ~高校生のSMごっこ~ 第1話 - new Regex( - @"第?(?\d+(?:\.\d+|-\d+)?)話", - MatchOptions, RegexTimeout), - // Russian Chapter: n Главa -> Chapter n - new Regex( - @"(?!Том)(?\d+(?:\.\d+|-\d+)?)(\s|_)(Глава|глава|Главы|Глава)", + @"(?.*(\d{4})?)( |_)(?:\((?\d+) of \d+)", MatchOptions, RegexTimeout), }; - private static readonly Regex MangaEditionRegex = new Regex( - // Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz - // To Love Ru v01 Uncensored (Ch.001-007) - @"\b(?:Omnibus(?:\s?Edition)?|Uncensored)\b", - MatchOptions, RegexTimeout - ); + #endregion + // Matches anything between balanced parenthesis, tags between brackets, {} and {Complete} private static readonly Regex CleanupRegex = new Regex( @@ -617,25 +663,6 @@ public static class Parser MatchOptions, RegexTimeout ); - private static readonly Regex MangaSpecialRegex = new Regex( - // All Keywords, does not account for checking if contains volume/chapter identification. Parser.Parse() will handle. - $@"\b(?:{CommonSpecial}|Omake)\b", - MatchOptions, RegexTimeout - ); - - private static readonly Regex ComicSpecialRegex = new Regex( - // All Keywords, does not account for checking if contains volume/chapter identification. Parser.Parse() will handle. - $@"\b(?:{CommonSpecial}|\d.+?(\W|-|^)Annual|Annual(\W|-|$)|Book \d.+?|Compendium(\W|-|$|\s.+?)|Omnibus(\W|-|$|\s.+?)|FCBD \d.+?|Absolute(\W|-|$|\s.+?)|Preview(\W|-|$|\s.+?)|Hors[ -]S[ée]rie|TPB|HS|THS)\b", - MatchOptions, RegexTimeout - ); - - private static readonly Regex EuropeanComicRegex = new Regex( - // All Keywords, does not account for checking if contains volume/chapter identification. Parser.Parse() will handle. - @"\b(?:Bd[-\s]Fr)\b", - 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+", @@ -714,6 +741,20 @@ public static class Parser return string.Empty; } + public static string ParseMagazineSeries(string filename) + { + foreach (var regex in MagazineSeriesRegex) + { + var matches = regex.Matches(filename); + var group = matches + .Select(match => match.Groups["Series"]) + .FirstOrDefault(group => group.Success && group != Match.Empty); + if (group != null) return CleanTitle(group.Value, true); + } + + return string.Empty; + } + public static string ParseVolume(string filename) { foreach (var regex in MangaVolumeRegex) @@ -750,6 +791,24 @@ public static class Parser return DefaultVolume; } + public static string ParseMagazineVolume(string filename) + { + foreach (var regex in MagazineVolumeRegex) + { + var matches = regex.Matches(filename); + foreach (var group in matches.Select(match => match.Groups)) + { + if (!group["Volume"].Success || group["Volume"] == Match.Empty) continue; + + var value = group["Volume"].Value; + var hasPart = group["Part"].Success; + return FormatValue(value, hasPart); + } + } + + return DefaultVolume; + } + private static string FormatValue(string value, bool hasPart) { if (!value.Contains('-')) diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/API/Services/Tasks/Scanner/ProcessSeries.cs index 7815a5bcf..03a65ace0 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/API/Services/Tasks/Scanner/ProcessSeries.cs @@ -122,6 +122,7 @@ public class ProcessSeries : IProcessSeries } catch (Exception ex) { + // TODO: Output more information to the user _logger.LogError(ex, "There was an exception finding existing series for {SeriesName} with Localized name of {LocalizedName} for library {LibraryId}. This indicates you have duplicate series with same name or localized name in the library. Correct this and rescan", firstInfo.Series, firstInfo.LocalizedSeries, library.Id); await _eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent($"There was an exception finding existing series for {firstInfo.Series} with Localized name of {firstInfo.LocalizedSeries} for library {library.Id}", diff --git a/UI/Web/src/app/_models/library/library.ts b/UI/Web/src/app/_models/library/library.ts index 76e463bb4..c42117af2 100644 --- a/UI/Web/src/app/_models/library/library.ts +++ b/UI/Web/src/app/_models/library/library.ts @@ -4,7 +4,8 @@ export enum LibraryType { Manga = 0, Comic = 1, Book = 2, - Images = 3 + Images = 3, + Magazine = 4 } export interface Library { diff --git a/UI/Web/src/app/_pipes/library-type.pipe.ts b/UI/Web/src/app/_pipes/library-type.pipe.ts index 4686175f4..f30b2a983 100644 --- a/UI/Web/src/app/_pipes/library-type.pipe.ts +++ b/UI/Web/src/app/_pipes/library-type.pipe.ts @@ -20,6 +20,8 @@ export class LibraryTypePipe implements PipeTransform { return this.translocoService.translate('library-type-pipe.comic'); case LibraryType.Manga: return this.translocoService.translate('library-type-pipe.manga'); + case LibraryType.Magazine: + return this.translocoService.translate('library-type-pipe.magazine'); default: return ''; } diff --git a/UI/Web/src/app/shared/_services/utility.service.ts b/UI/Web/src/app/shared/_services/utility.service.ts index 8c2382cb5..a6a3c146d 100644 --- a/UI/Web/src/app/shared/_services/utility.service.ts +++ b/UI/Web/src/app/shared/_services/utility.service.ts @@ -1,11 +1,11 @@ -import { HttpParams } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { Chapter } from 'src/app/_models/chapter'; -import { LibraryType } from 'src/app/_models/library/library'; -import { MangaFormat } from 'src/app/_models/manga-format'; -import { PaginatedResult } from 'src/app/_models/pagination'; -import { Series } from 'src/app/_models/series'; -import { Volume } from 'src/app/_models/volume'; +import {HttpParams} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {Chapter} from 'src/app/_models/chapter'; +import {LibraryType} from 'src/app/_models/library/library'; +import {MangaFormat} from 'src/app/_models/manga-format'; +import {PaginatedResult} from 'src/app/_models/pagination'; +import {Series} from 'src/app/_models/series'; +import {Volume} from 'src/app/_models/volume'; import {TranslocoService} from "@ngneat/transloco"; export enum KEY_CODES { @@ -63,6 +63,7 @@ export class UtilityService { */ formatChapterName(libraryType: LibraryType, includeHash: boolean = false, includeSpace: boolean = false) { switch(libraryType) { + case LibraryType.Magazine: // TODO: Figure out if we need something special case LibraryType.Book: return this.translocoService.translate('common.book-num') + (includeSpace ? ' ' : ''); case LibraryType.Comic: diff --git a/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.ts b/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.ts index 8e4de95b0..a2d66c144 100644 --- a/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.ts +++ b/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.ts @@ -188,6 +188,8 @@ export class SideNavComponent implements OnInit { return 'fa-book-open'; case LibraryType.Images: return 'fa-images'; + case LibraryType.Magazine: + return 'fa-book-open'; // TODO: Find an icon for this } } diff --git a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts index e9de2a4b2..189b592f2 100644 --- a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts +++ b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts @@ -179,6 +179,12 @@ export class LibrarySettingsModalComponent implements OnInit { this.libraryForm.get(FileTypeGroup.Pdf + '')?.setValue(false); this.libraryForm.get(FileTypeGroup.Epub + '')?.setValue(false); break; + case LibraryType.Magazine: + this.libraryForm.get(FileTypeGroup.Archive + '')?.setValue(true); + this.libraryForm.get(FileTypeGroup.Images + '')?.setValue(false); + this.libraryForm.get(FileTypeGroup.Pdf + '')?.setValue(true); + this.libraryForm.get(FileTypeGroup.Epub + '')?.setValue(false); + break; } }), takeUntilDestroyed(this.destroyRef) diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index f45e07a51..65dd1317b 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -488,7 +488,8 @@ "library-type-pipe": { "book": "Book", "comic": "Comic", - "manga": "Manga" + "manga": "Manga", + "magazine": "Magazine" }, "age-rating-pipe": { diff --git a/openapi.json b/openapi.json index 63452f70f..e98f37d7a 100644 --- a/openapi.json +++ b/openapi.json @@ -2956,7 +2956,8 @@ 0, 1, 2, - 3 + 3, + 4 ], "type": "integer", "format": "int32" @@ -2968,7 +2969,8 @@ 0, 1, 2, - 3 + 3, + 4 ], "type": "integer", "format": "int32" @@ -2980,7 +2982,8 @@ 0, 1, 2, - 3 + 3, + 4 ], "type": "integer", "format": "int32" @@ -13539,7 +13542,8 @@ 0, 1, 2, - 3 + 3, + 4 ], "type": "integer", "format": "int32" @@ -14131,7 +14135,8 @@ 0, 1, 2, - 3 + 3, + 4 ], "type": "integer", "description": "Library type", @@ -15634,7 +15639,8 @@ 0, 1, 2, - 3 + 3, + 4 ], "type": "integer", "format": "int32" @@ -15747,7 +15753,8 @@ 0, 1, 2, - 3 + 3, + 4 ], "type": "integer", "format": "int32" @@ -16754,7 +16761,8 @@ 0, 1, 2, - 3 + 3, + 4 ], "type": "integer", "format": "int32" @@ -16806,7 +16814,8 @@ 0, 1, 2, - 3 + 3, + 4 ], "type": "integer", "format": "int32" @@ -19071,7 +19080,8 @@ 0, 1, 2, - 3 + 3, + 4 ], "type": "integer", "format": "int32" From 5a522b6d5bb391a14842f42506991f37c359937a Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sat, 10 Feb 2024 13:10:06 -0600 Subject: [PATCH 2/6] Setup the parsing rules for Magazines. --- API.Tests/Parser/MagazineParserTests.cs | 29 +++++- API/Services/Tasks/Scanner/Parser/Parser.cs | 105 ++++++++++++++++++-- openapi.json | 35 ++++--- 3 files changed, 147 insertions(+), 22 deletions(-) diff --git a/API.Tests/Parser/MagazineParserTests.cs b/API.Tests/Parser/MagazineParserTests.cs index 417d6e1b9..53735632e 100644 --- a/API.Tests/Parser/MagazineParserTests.cs +++ b/API.Tests/Parser/MagazineParserTests.cs @@ -6,15 +6,38 @@ public class MagazineParserTests { [Theory] [InlineData("3D World - 2018 UK", "3D World")] + [InlineData("3D World - 2018", "3D World")] + [InlineData("UK World - 022012 [Digital]", "UK World")] + [InlineData("Computer Weekly - September 2023", "Computer Weekly")] public void ParseSeriesTest(string filename, string expected) { Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseMagazineSeries(filename)); } + [Theory] + [InlineData("UK World - 022012 [Digital]", "2012")] + [InlineData("Computer Weekly - September 2023", "2023")] + [InlineData("Computer Weekly - September 2023 #2", "2023")] + [InlineData("PC Games - 2001 #01", "2001")] + public void ParseVolumeTest(string filename, string expected) + { + Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseMagazineVolume(filename)); + } + + [Theory] + [InlineData("UK World - 022012 [Digital]", "0")] + [InlineData("Computer Weekly - September 2023", "9")] + [InlineData("Computer Weekly - September 2023 #2", "2")] + [InlineData("PC Games - 2001 #01", "1")] + public void ParseChapterTest(string filename, string expected) + { + Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseMagazineChapter(filename)); + } + // [Theory] - // [InlineData("Harrison, Kim - Dates from Hell - Hollows Vol 2.5.epub", "2.5")] - // public void ParseVolumeTest(string filename, string expected) + // [InlineData("AIR International Vol. 14 No. 3 (ISSN 1011-3250)", "1011-3250")] + // public void ParseGTINTest(string filename, string expected) // { - // Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseMagazineVolume(filename)); + // Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseGTIN(filename)); // } } diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index 4d49252d5..cce578aed 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Collections.Immutable; +using System.Globalization; using System.IO; using System.Linq; using System.Text.RegularExpressions; @@ -9,7 +11,7 @@ using API.Extensions; namespace API.Services.Tasks.Scanner.Parser; #nullable enable -public static class Parser +public static partial class Parser { public const string DefaultChapter = "0"; public const string DefaultVolume = "0"; @@ -59,6 +61,8 @@ public static class Parser /// private const string CommonSpecial = @"Specials?|One[- ]?Shot|Extra(?:\sChapter)?(?=\s)|Art Collection|Side Stories|Bonus"; + [GeneratedRegex(@"^\d+$")] + private static partial Regex IsNumberRegex(); /// /// Matches against font-family css syntax. Does not match if url import has data: starting, as that is binary data @@ -634,13 +638,17 @@ public static class Parser #region Magazine + private static readonly Dictionary _monthMappings = CreateMonthMappings(); private static readonly Regex[] MagazineSeriesRegex = new[] { - // 3D World - 2018 UK + // 3D World - 2018 UK, 3D World - 022014 new Regex( - @"(?.+?)(\b|_|\s)?-(\b|_|\s)(?\d{4}).+", + @"^(?.+?)(_|\s)*-(_|\s)*\d{4,6}.*", MatchOptions, RegexTimeout), // AIR International - April 2018 UK + new Regex( + @"^(?.+?)(_|\s)*-(_|\s)*.*", + MatchOptions, RegexTimeout), // The New Yorker - April 2, 2018 USA // AIR International Magazine 2006 // AIR International Vol. 14 No. 3 (ISSN 1011-3250) @@ -648,9 +656,34 @@ public static class Parser private static readonly Regex[] MagazineVolumeRegex = new[] { - // Batman & Wildcat (1 of 3) + // 3D World - 2018 UK, 3D World - 022014 new Regex( - @"(?.*(\d{4})?)( |_)(?:\((?\d+) of \d+)", + @"^(?.+?)(_|\s)*-(_|\s)*\d{2}?(?\d{4}).*", + MatchOptions, RegexTimeout), + // 3D World - Sept 2018 + new Regex( + @"^(?.+?)(_|\s)*-(_|\s)*\D+(?\d{4}).*", + MatchOptions, RegexTimeout), + // 3D World - Sept 2018 + new Regex( + @"^(?.+?)(_|\s)*-(_|\s)*\D+(?\d{4}).*", + MatchOptions, RegexTimeout), + + }; + + private static readonly Regex[] MagazineChapterRegex = new[] + { + // 3D World - September 2023 #2 + new Regex( + @"^(?.+?)(_|\s)*-(_|\s)*.*#(?\d+).*", + MatchOptions, RegexTimeout), + // Computer Weekly - September 2023 + new Regex( + @"^(?.+?)(_|\s)*-(_|\s)*(?January|February|March|April|May|June|July|August|September|October|November|December).*", + MatchOptions, RegexTimeout), + // Computer Weekly - Sept 2023 + new Regex( + @"^(?.+?)(_|\s)*-(_|\s)*(?Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sept|Oct|Nov|Dec).*", MatchOptions, RegexTimeout), }; @@ -801,14 +834,71 @@ public static class Parser if (!group["Volume"].Success || group["Volume"] == Match.Empty) continue; var value = group["Volume"].Value; - var hasPart = group["Part"].Success; - return FormatValue(value, hasPart); + return FormatValue(value, false); } } return DefaultVolume; } + + private static Dictionary CreateMonthMappings() + { + Dictionary mappings = new(StringComparer.OrdinalIgnoreCase); + + // Add English month names and shorthands + for (var i = 1; i <= 12; i++) + { + var month = new DateTime(2022, i, 1); + var monthName = month.ToString("MMMM", CultureInfo.InvariantCulture); + var monthAbbreviation = month.ToString("MMM", CultureInfo.InvariantCulture); + + mappings[monthName] = i; + mappings[monthAbbreviation] = i; + } + + // Add mappings for other languages if needed + // Example: mappings["KoreanMonthName"] = correspondingNumericalValue; + + return mappings; + } + + static int ConvertMonthToNumber(string month, Dictionary monthMappings) + { + // Check if the month exists in the mappings + if (monthMappings.TryGetValue(month, out int numericalValue)) + { + return numericalValue; + } + + // If the month is not found in mappings, you may handle other cases here, + // such as trying to parse non-English month names or returning a default value. + // For simplicity, we'll return 0 indicating failure. + return 0; + } + + public static string ParseMagazineChapter(string filename) + { + foreach (var regex in MagazineChapterRegex) + { + var matches = regex.Matches(filename); + foreach (var groups in matches.Select(match => match.Groups)) + { + if (!groups["Chapter"].Success || groups["Chapter"] == Match.Empty) continue; + + var value = groups["Chapter"].Value; + // If value has non-digits, we need to convert to a digit + if (IsNumberRegex().IsMatch(value)) return FormatValue(value, false); + if (_monthMappings.TryGetValue(value, out var parsedMonth)) + { + return FormatValue($"{parsedMonth}", false); + } + } + } + + return DefaultChapter; + } + private static string FormatValue(string value, bool hasPart) { if (!value.Contains('-')) @@ -1155,4 +1245,5 @@ public static class Parser return null; } + } diff --git a/openapi.json b/openapi.json index 35b9b8ef0..0dbb8432a 100644 --- a/openapi.json +++ b/openapi.json @@ -7,7 +7,7 @@ "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "0.7.14.1" + "version": "0.7.14.2" }, "servers": [ { @@ -2909,7 +2909,8 @@ 1, 2, 3, - 4 + 4, + 5 ], "type": "integer", "format": "int32" @@ -2922,7 +2923,8 @@ 1, 2, 3, - 4 + 4, + 5 ], "type": "integer", "format": "int32" @@ -2935,7 +2937,8 @@ 1, 2, 3, - 4 + 4, + 5 ], "type": "integer", "format": "int32" @@ -3619,7 +3622,8 @@ 1, 2, 3, - 4 + 4, + 5 ], "type": "integer", "format": "int32" @@ -13526,7 +13530,8 @@ 1, 2, 3, - 4 + 4, + 5 ], "type": "integer", "format": "int32" @@ -14119,7 +14124,8 @@ 1, 2, 3, - 4 + 4, + 5 ], "type": "integer", "description": "Library type", @@ -15885,7 +15891,8 @@ 1, 2, 3, - 4 + 4, + 5 ], "type": "integer", "format": "int32" @@ -15999,7 +16006,8 @@ 1, 2, 3, - 4 + 4, + 5 ], "type": "integer", "format": "int32" @@ -16989,7 +16997,8 @@ 1, 2, 3, - 4 + 4, + 5 ], "type": "integer", "format": "int32" @@ -17042,7 +17051,8 @@ 1, 2, 3, - 4 + 4, + 5 ], "type": "integer", "format": "int32" @@ -19381,7 +19391,8 @@ 1, 2, 3, - 4 + 4, + 5 ], "type": "integer", "format": "int32" From 95e7ad0f5b82561e45170231e319b4f002e1cfb2 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sat, 10 Feb 2024 16:39:37 -0600 Subject: [PATCH 3/6] Basic fallback parsing code is in place. --- API/Services/SeriesService.cs | 18 +++++++------ .../Tasks/Scanner/Parser/DefaultParser.cs | 27 ++++++++++++++++--- API/Services/Tasks/Scanner/Parser/Parser.cs | 26 ++++++++---------- UI/Web/src/app/_models/library/library.ts | 1 - .../series-detail/series-detail.component.ts | 11 ++++++++ .../app/shared/_services/utility.service.ts | 2 +- 6 files changed, 56 insertions(+), 29 deletions(-) diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index b7556655f..1ecdd7096 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -537,18 +537,18 @@ public class SeriesService : ISeriesService retChapters = Array.Empty(); } else { - retChapters = chapters - .Where(ShouldIncludeChapter); + retChapters = chapters.Where(ShouldIncludeChapter); } - var storylineChapters = volumes - .Where(v => v.MinNumber == 0) - .SelectMany(v => v.Chapters.Where(c => !c.IsSpecial)) - .OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default) - .ToList(); + var storylineChapters = libraryType == LibraryType.Magazine ? [] + : volumes + .Where(v => v.MinNumber == 0) + .SelectMany(v => v.Chapters.Where(c => !c.IsSpecial)) + .OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default) + .ToList(); // When there's chapters without a volume number revert to chapter sorting only as opposed to volume then chapter - if (storylineChapters.Any()) { + if (storylineChapters.Count > 0) { retChapters = retChapters.OrderBy(c => c.Number.AsFloat(), ChapterSortComparer.Default); } @@ -615,8 +615,10 @@ public class SeriesService : ISeriesService { LibraryType.Book => await _localizationService.Translate(userId, "book-num", chapterTitle), LibraryType.LightNovel => await _localizationService.Translate(userId, "book-num", chapterTitle), + LibraryType.Magazine => await _localizationService.Translate(userId, "issue-num", hashSpot, chapterTitle), LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", hashSpot, chapterTitle), LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", chapterTitle), + LibraryType.Image => await _localizationService.Translate(userId, "chapter-num", chapterTitle), _ => await _localizationService.Translate(userId, "chapter-num", ' ') }; } diff --git a/API/Services/Tasks/Scanner/Parser/DefaultParser.cs b/API/Services/Tasks/Scanner/Parser/DefaultParser.cs index 643f8a9ce..1a08be2dd 100644 --- a/API/Services/Tasks/Scanner/Parser/DefaultParser.cs +++ b/API/Services/Tasks/Scanner/Parser/DefaultParser.cs @@ -125,13 +125,32 @@ public class DefaultParser : IDefaultParser { // Try to parse Series from the filename var libraryPath = _directoryService.FileSystem.DirectoryInfo.New(rootPath).Parent?.FullName ?? rootPath; - ret.Series = Parser.ParseMagazineSeries(filePath); - if (string.IsNullOrEmpty(ret.Series)) + var fileName = _directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath); + ret.Series = Parser.ParseMagazineSeries(fileName); + ret.Volumes = Parser.ParseMagazineVolume(fileName); + ret.Chapters = Parser.ParseMagazineChapter(fileName); + + if (string.IsNullOrEmpty(ret.Series) || (string.IsNullOrEmpty(ret.Chapters) && string.IsNullOrEmpty(ret.Volumes))) { // Fallback to the parent folder. We can also likely grab Volume (year) from here - var folders = _directoryService.GetFoldersTillRoot(libraryPath, filePath); - foreach (var folder in folders) + var folders = _directoryService.GetFoldersTillRoot(libraryPath, filePath).ToList(); + // Usually the LAST folder is the Series and everything up to can have Volume + + if (string.IsNullOrEmpty(ret.Series)) { + ret.Series = Parser.CleanTitle(folders[^1]); + } + foreach (var folder in folders[..^1]) + { + if (ret.Volumes == Parser.DefaultVolume) + { + var vol = Parser.ParseYear(folder); + if (vol != folder) + { + ret.Volumes = vol; + } + } + } } diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index cce578aed..7909b5edc 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -687,6 +687,11 @@ public static partial class Parser MatchOptions, RegexTimeout), }; + private static readonly Regex YearRegex = new Regex( + @"(\b|\s|_)[1-9]{1}\d{3}(\b|\s|_)", + MatchOptions, RegexTimeout + ); + #endregion @@ -858,25 +863,10 @@ public static partial class Parser } // Add mappings for other languages if needed - // Example: mappings["KoreanMonthName"] = correspondingNumericalValue; return mappings; } - static int ConvertMonthToNumber(string month, Dictionary monthMappings) - { - // Check if the month exists in the mappings - if (monthMappings.TryGetValue(month, out int numericalValue)) - { - return numericalValue; - } - - // If the month is not found in mappings, you may handle other cases here, - // such as trying to parse non-English month names or returning a default value. - // For simplicity, we'll return 0 indicating failure. - return 0; - } - public static string ParseMagazineChapter(string filename) { foreach (var regex in MagazineChapterRegex) @@ -899,6 +889,12 @@ public static partial class Parser return DefaultChapter; } + public static string ParseYear(string value) + { + if (string.IsNullOrEmpty(value)) return value; + return YearRegex.Match(value).Value; + } + private static string FormatValue(string value, bool hasPart) { if (!value.Contains('-')) diff --git a/UI/Web/src/app/_models/library/library.ts b/UI/Web/src/app/_models/library/library.ts index 30ee14819..d5ea0da8a 100644 --- a/UI/Web/src/app/_models/library/library.ts +++ b/UI/Web/src/app/_models/library/library.ts @@ -5,7 +5,6 @@ export enum LibraryType { Comic = 1, Book = 2, Images = 3, - Images = 3, LightNovel = 4, Magazine = 5 } diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts index ca022e22a..5071d7b0e 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts @@ -338,6 +338,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { } get ShowStorylineTab() { + if (this.libraryType === LibraryType.Magazine) return false; return (this.libraryType !== LibraryType.Book && this.libraryType !== LibraryType.LightNovel) && (this.volumes.length > 0 || this.chapters.length > 0); } @@ -716,6 +717,16 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { */ updateSelectedTab() { // Book libraries only have Volumes or Specials enabled + if (this.libraryType === LibraryType.Magazine) { + if (this.volumes.length === 0) { + this.activeTabId = TabID.Chapters; + } else { + this.activeTabId = TabID.Volumes; + } + this.cdRef.markForCheck(); + return; + } + if (this.libraryType === LibraryType.Book || this.libraryType === LibraryType.LightNovel) { if (this.volumes.length === 0) { this.activeTabId = TabID.Specials; diff --git a/UI/Web/src/app/shared/_services/utility.service.ts b/UI/Web/src/app/shared/_services/utility.service.ts index f6851fe42..1a9ff4a78 100644 --- a/UI/Web/src/app/shared/_services/utility.service.ts +++ b/UI/Web/src/app/shared/_services/utility.service.ts @@ -63,10 +63,10 @@ export class UtilityService { */ formatChapterName(libraryType: LibraryType, includeHash: boolean = false, includeSpace: boolean = false) { switch(libraryType) { - case LibraryType.Magazine: // TODO: Figure out if we need something special case LibraryType.Book: case LibraryType.LightNovel: return this.translocoService.translate('common.book-num') + (includeSpace ? ' ' : ''); + case LibraryType.Magazine: // TODO: Figure out if we need something special case LibraryType.Comic: if (includeHash) { return this.translocoService.translate('common.issue-hash-num'); From b3f6a574cd8be31a626355182215f883c7694c5a Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sun, 11 Feb 2024 14:30:13 -0600 Subject: [PATCH 4/6] Lots of changes to get magazines working. Some notes is that magazines in reading list mode do not properly load up. Parsing code still needs some work. Need to restrict to really just a small set of conventions until community can give me real data to code against. --- API/Controllers/BookController.cs | 1 + API/Controllers/LibraryController.cs | 11 +++ API/Controllers/ReaderController.cs | 2 + API/DTOs/LibraryTypeDto.cs | 12 +++ API/DTOs/Reader/BookInfoDto.cs | 1 + API/DTOs/Reader/IChapterInfoDto.cs | 1 + API/Data/Repositories/ChapterRepository.cs | 4 +- API/Data/Repositories/LibraryRepository.cs | 14 ++++ .../Tasks/Scanner/Parser/DefaultParser.cs | 18 ++++- API/Services/Tasks/Scanner/Parser/Parser.cs | 57 ++++++++++++-- UI/Web/src/app/_services/library.service.ts | 14 ++++ UI/Web/src/app/_services/reader.service.ts | 40 +++++----- UI/Web/src/app/app.component.ts | 3 +- .../book-reader/book-reader.component.ts | 22 +++--- .../card-detail-drawer.component.ts | 2 +- .../manga-reader/manga-reader.component.ts | 68 ++++++++++++++--- .../reading-list-detail.component.ts | 9 ++- .../series-detail/series-detail.component.ts | 9 ++- .../reading-activity.component.html | 2 +- .../server-stats/server-stats.component.html | 8 +- .../user-preferences.component.ts | 1 - UI/Web/src/assets/langs/en.json | 4 + openapi.json | 74 +++++++++++++++++++ 23 files changed, 315 insertions(+), 62 deletions(-) create mode 100644 API/DTOs/LibraryTypeDto.cs diff --git a/API/Controllers/BookController.cs b/API/Controllers/BookController.cs index 962500ec7..623f145ed 100644 --- a/API/Controllers/BookController.cs +++ b/API/Controllers/BookController.cs @@ -82,6 +82,7 @@ public class BookController : BaseApiController SeriesFormat = dto.SeriesFormat, SeriesId = dto.SeriesId, LibraryId = dto.LibraryId, + LibraryType = dto.LibraryType, IsSpecial = dto.IsSpecial, Pages = dto.Pages, }); diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index b4b86dccf..57e30ad02 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; @@ -504,4 +505,14 @@ public class LibraryController : BaseApiController { return Ok(await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(libraryId)); } + + /// + /// Return pairs of all types + /// + /// + [HttpGet("types")] + public async Task>> GetLibraryTypes() + { + return Ok(await _unitOfWork.LibraryRepository.GetLibraryTypesAsync(User.GetUserId())); + } } diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 89078487d..3d7ac75bf 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -244,6 +244,7 @@ public class ReaderController : BaseApiController SeriesFormat = dto.SeriesFormat, SeriesId = dto.SeriesId, LibraryId = dto.LibraryId, + LibraryType = dto.LibraryType, IsSpecial = dto.IsSpecial, Pages = dto.Pages, SeriesTotalPages = series.Pages, @@ -284,6 +285,7 @@ public class ReaderController : BaseApiController return Ok(info); } + /// /// Returns various information about all bookmark files for a Series. Side effect: This will cache the bookmark images for reading. /// diff --git a/API/DTOs/LibraryTypeDto.cs b/API/DTOs/LibraryTypeDto.cs new file mode 100644 index 000000000..1f23c604b --- /dev/null +++ b/API/DTOs/LibraryTypeDto.cs @@ -0,0 +1,12 @@ +using API.Entities.Enums; + +namespace API.DTOs; + +/// +/// Simple pairing of LibraryId and LibraryType +/// +public class LibraryTypeDto +{ + public int LibraryId { get; set; } + public LibraryType LibraryType { get; set; } +} diff --git a/API/DTOs/Reader/BookInfoDto.cs b/API/DTOs/Reader/BookInfoDto.cs index c379f71f8..3e5cc30dd 100644 --- a/API/DTOs/Reader/BookInfoDto.cs +++ b/API/DTOs/Reader/BookInfoDto.cs @@ -15,4 +15,5 @@ public class BookInfoDto : IChapterInfoDto public int Pages { get; set; } public bool IsSpecial { get; set; } public string ChapterTitle { get; set; } = default! ; + public LibraryType LibraryType { get; set; } } diff --git a/API/DTOs/Reader/IChapterInfoDto.cs b/API/DTOs/Reader/IChapterInfoDto.cs index 6a9a74a2c..568adf345 100644 --- a/API/DTOs/Reader/IChapterInfoDto.cs +++ b/API/DTOs/Reader/IChapterInfoDto.cs @@ -14,5 +14,6 @@ public interface IChapterInfoDto public int Pages { get; set; } public bool IsSpecial { get; set; } public string ChapterTitle { get; set; } + public LibraryType LibraryType { get; set; } } diff --git a/API/Data/Repositories/ChapterRepository.cs b/API/Data/Repositories/ChapterRepository.cs index a9fbf3ce3..a8bb699ff 100644 --- a/API/Data/Repositories/ChapterRepository.cs +++ b/API/Data/Repositories/ChapterRepository.cs @@ -112,11 +112,11 @@ public class ChapterRepository : IChapterRepository LibraryId = data.LibraryId, Pages = data.Pages, ChapterTitle = data.TitleName, - LibraryType = data.LibraryType + LibraryType = data.LibraryType, }) .AsNoTracking() .AsSplitQuery() - .SingleOrDefaultAsync(); + .FirstOrDefaultAsync(); return chapterInfo; } diff --git a/API/Data/Repositories/LibraryRepository.cs b/API/Data/Repositories/LibraryRepository.cs index d6d562b82..bc19c5312 100644 --- a/API/Data/Repositories/LibraryRepository.cs +++ b/API/Data/Repositories/LibraryRepository.cs @@ -57,6 +57,7 @@ public interface ILibraryRepository Task GetAllowsScrobblingBySeriesId(int seriesId); Task> GetLibraryTypesBySeriesIdsAsync(IList seriesIds); + Task> GetLibraryTypesAsync(int userId); } public class LibraryRepository : ILibraryRepository @@ -365,4 +366,17 @@ public class LibraryRepository : ILibraryRepository }) .ToDictionaryAsync(entity => entity.Id, entity => entity.Type); } + + public async Task> GetLibraryTypesAsync(int userId) + { + return await _context.Library + .Where(l => l.AppUsers.Any(u => u.Id == userId)) + .Select(l => new LibraryTypeDto() + { + LibraryType = l.Type, + LibraryId = l.Id + }) + .AsSplitQuery() + .ToListAsync(); + } } diff --git a/API/Services/Tasks/Scanner/Parser/DefaultParser.cs b/API/Services/Tasks/Scanner/Parser/DefaultParser.cs index 1a08be2dd..3c25cc73e 100644 --- a/API/Services/Tasks/Scanner/Parser/DefaultParser.cs +++ b/API/Services/Tasks/Scanner/Parser/DefaultParser.cs @@ -34,6 +34,8 @@ public class DefaultParser : IDefaultParser public ParserInfo? Parse(string filePath, string rootPath, LibraryType type = LibraryType.Manga) { var fileName = _directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath); + + // We can now remove this as there is the ability to turn off images for non-image libraries // TODO: Potential Bug: This will return null, but on Image libraries, if all images, we would want to include this. if (type != LibraryType.Image && Parser.IsCoverImage(_directoryService.FileSystem.Path.GetFileName(filePath))) return null; @@ -49,7 +51,7 @@ public class DefaultParser : IDefaultParser // If library type is Image or this is not a cover image in a non-image library, then use dedicated parsing mechanism if (type == LibraryType.Image || Parser.IsImage(filePath)) { - // TODO: We can move this up one level + // TODO: We can move this up one level (out of DefaultParser - If we do different Parsers) return ParseImage(filePath, rootPath, ret); } @@ -136,21 +138,33 @@ public class DefaultParser : IDefaultParser var folders = _directoryService.GetFoldersTillRoot(libraryPath, filePath).ToList(); // Usually the LAST folder is the Series and everything up to can have Volume + if (string.IsNullOrEmpty(ret.Series)) { ret.Series = Parser.CleanTitle(folders[^1]); } + var hasGeoCode = !string.IsNullOrEmpty(Parser.ParseGeoCode(ret.Series)); foreach (var folder in folders[..^1]) { if (ret.Volumes == Parser.DefaultVolume) { var vol = Parser.ParseYear(folder); - if (vol != folder) + if (!string.IsNullOrEmpty(vol) && vol != folder) { ret.Volumes = vol; } } + // If folder has a language code in it, then we add that to the Series (Wired (UK)) + if (!hasGeoCode) + { + var geoCode = Parser.ParseGeoCode(folder); + if (!string.IsNullOrEmpty(geoCode)) + { + ret.Series = $"{ret.Series} ({geoCode})"; + hasGeoCode = true; + } + } } } diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index 7909b5edc..fa1a042e2 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -638,9 +638,10 @@ public static partial class Parser #region Magazine - private static readonly Dictionary _monthMappings = CreateMonthMappings(); - private static readonly Regex[] MagazineSeriesRegex = new[] - { + private static readonly HashSet GeoCodes = new(CreateCountryCodes()); + private static readonly Dictionary MonthMappings = CreateMonthMappings(); + private static readonly Regex[] MagazineSeriesRegex = + [ // 3D World - 2018 UK, 3D World - 022014 new Regex( @"^(?.+?)(_|\s)*-(_|\s)*\d{4,6}.*", @@ -649,10 +650,14 @@ public static partial class Parser new Regex( @"^(?.+?)(_|\s)*-(_|\s)*.*", MatchOptions, RegexTimeout), + // AIR International #1 // This breaks the way the code works + // new Regex( + // @"^(?.+?)(_|\s)+?#", + // MatchOptions, RegexTimeout) // The New Yorker - April 2, 2018 USA // AIR International Magazine 2006 // AIR International Vol. 14 No. 3 (ISSN 1011-3250) - }; + ]; private static readonly Regex[] MagazineVolumeRegex = new[] { @@ -846,6 +851,17 @@ public static partial class Parser return DefaultVolume; } + private static string[] CreateCountryCodes() + { + var codes = CultureInfo.GetCultures(CultureTypes.SpecificCultures) + .Select(culture => new RegionInfo(culture.Name).TwoLetterISORegionName) + .Distinct() + .OrderBy(code => code) + .ToList(); + codes.Add("UK"); + return codes.ToArray(); + } + private static Dictionary CreateMonthMappings() { @@ -879,7 +895,7 @@ public static partial class Parser var value = groups["Chapter"].Value; // If value has non-digits, we need to convert to a digit if (IsNumberRegex().IsMatch(value)) return FormatValue(value, false); - if (_monthMappings.TryGetValue(value, out var parsedMonth)) + if (MonthMappings.TryGetValue(value, out var parsedMonth)) { return FormatValue($"{parsedMonth}", false); } @@ -889,7 +905,36 @@ public static partial class Parser return DefaultChapter; } - public static string ParseYear(string value) + /// + /// Tries to parse a GeoCode (UK, US) out of a string + /// + /// + /// + public static string? ParseGeoCode(string? value) + { + if (string.IsNullOrEmpty(value)) return value; + const string pattern = @"\b(?:\(|\[|\{)([A-Z]{2})(?:\)|\]|\})\b|^([A-Z]{2})$"; + + // Match the pattern in the input string + var match = Regex.Match(value, pattern, RegexOptions.IgnoreCase); + + if (match.Success) + { + // Extract the GeoCode from the first capturing group if it exists, + // otherwise, extract the GeoCode from the second capturing group + var extractedCode = match.Groups[1].Success ? match.Groups[1].Value : match.Groups[2].Value; + + // Validate the extracted GeoCode against the list of valid GeoCodes + if (GeoCodes.Contains(extractedCode)) + { + return extractedCode; + } + } + + return null; + } + + public static string? ParseYear(string? value) { if (string.IsNullOrEmpty(value)) return value; return YearRegex.Match(value).Value; diff --git a/UI/Web/src/app/_services/library.service.ts b/UI/Web/src/app/_services/library.service.ts index 75abf3a03..18cb55027 100644 --- a/UI/Web/src/app/_services/library.service.ts +++ b/UI/Web/src/app/_services/library.service.ts @@ -107,6 +107,20 @@ export class LibraryService { return this.httpClient.post(this.baseUrl + 'library/update', model); } + getLibraryTypes() { + if (this.libraryTypes) return of(this.libraryTypes); + return this.httpClient.get>(this.baseUrl + 'library/types').pipe(map(types => { + if (this.libraryTypes === undefined) { + this.libraryTypes = {}; + } + types.forEach(t => { + this.libraryTypes![t.libraryId] = t.libraryType; + }); + + return this.libraryTypes; + })); + } + getLibraryType(libraryId: number) { if (this.libraryTypes != undefined && this.libraryTypes.hasOwnProperty(libraryId)) { return of(this.libraryTypes[libraryId]); diff --git a/UI/Web/src/app/_services/reader.service.ts b/UI/Web/src/app/_services/reader.service.ts index bd91c78cb..d3754d5f3 100644 --- a/UI/Web/src/app/_services/reader.service.ts +++ b/UI/Web/src/app/_services/reader.service.ts @@ -1,23 +1,24 @@ -import { HttpClient, HttpParams } from '@angular/common/http'; +import {HttpClient} from '@angular/common/http'; import {DestroyRef, Inject, inject, Injectable} from '@angular/core'; import {DOCUMENT, Location} from '@angular/common'; -import { Router } from '@angular/router'; -import { environment } from 'src/environments/environment'; -import { ChapterInfo } from '../manga-reader/_models/chapter-info'; -import { Chapter } from '../_models/chapter'; -import { HourEstimateRange } from '../_models/series-detail/hour-estimate-range'; -import { MangaFormat } from '../_models/manga-format'; -import { BookmarkInfo } from '../_models/manga-reader/bookmark-info'; -import { PageBookmark } from '../_models/readers/page-bookmark'; -import { ProgressBookmark } from '../_models/readers/progress-bookmark'; -import { FileDimension } from '../manga-reader/_models/file-dimension'; +import {Router} from '@angular/router'; +import {environment} from 'src/environments/environment'; +import {ChapterInfo} from '../manga-reader/_models/chapter-info'; +import {Chapter} from '../_models/chapter'; +import {HourEstimateRange} from '../_models/series-detail/hour-estimate-range'; +import {MangaFormat} from '../_models/manga-format'; +import {BookmarkInfo} from '../_models/manga-reader/bookmark-info'; +import {PageBookmark} from '../_models/readers/page-bookmark'; +import {ProgressBookmark} from '../_models/readers/progress-bookmark'; +import {FileDimension} from '../manga-reader/_models/file-dimension'; import screenfull from 'screenfull'; -import { TextResonse } from '../_types/text-response'; -import { AccountService } from './account.service'; +import {TextResonse} from '../_types/text-response'; +import {AccountService} from './account.service'; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {PersonalToC} from "../_models/readers/personal-toc"; import {SeriesFilterV2} from "../_models/metadata/v2/series-filter-v2"; import NoSleep from 'nosleep.js'; +import {LibraryType} from "../_models/library/library"; export const CHAPTER_ID_DOESNT_EXIST = -1; @@ -72,12 +73,15 @@ export class ReaderService { } - getNavigationArray(libraryId: number, seriesId: number, chapterId: number, format: MangaFormat) { + getNavigationArray(libraryId: number, seriesId: number, chapterId: number, format: MangaFormat, libraryType: LibraryType) { if (format === undefined) format = MangaFormat.ARCHIVE; if (format === MangaFormat.EPUB) { return ['library', libraryId, 'series', seriesId, 'book', chapterId]; } else if (format === MangaFormat.PDF) { + if (libraryType === LibraryType.Magazine) { + return ['library', libraryId, 'series', seriesId, 'manga', chapterId]; + } return ['library', libraryId, 'series', seriesId, 'pdf', chapterId]; } else { return ['library', libraryId, 'series', seriesId, 'manga', chapterId]; @@ -131,8 +135,8 @@ export class ReaderService { return this.httpClient.get(this.baseUrl + 'reader/get-progress?chapterId=' + chapterId); } - getPageUrl(chapterId: number, page: number) { - return `${this.baseUrl}reader/image?chapterId=${chapterId}&apiKey=${this.encodedKey}&page=${page}`; + getPageUrl(chapterId: number, page: number, extractPdf = false) { + return `${this.baseUrl}reader/image?chapterId=${chapterId}&apiKey=${this.encodedKey}&page=${page}&extractPdf=${extractPdf}`; } getThumbnailUrl(chapterId: number, page: number) { @@ -143,8 +147,8 @@ export class ReaderService { return this.baseUrl + 'reader/bookmark-image?seriesId=' + seriesId + '&page=' + page + '&apiKey=' + encodeURIComponent(apiKey); } - getChapterInfo(chapterId: number, includeDimensions = false) { - return this.httpClient.get(this.baseUrl + 'reader/chapter-info?chapterId=' + chapterId + '&includeDimensions=' + includeDimensions); + getChapterInfo(chapterId: number, includeDimensions = false, extractPdf = false) { + return this.httpClient.get(this.baseUrl + `reader/chapter-info?chapterId=${chapterId}&includeDimensions=${includeDimensions}&extractPdf=${extractPdf}`); } getFileDimensions(chapterId: number) { diff --git a/UI/Web/src/app/app.component.ts b/UI/Web/src/app/app.component.ts index 941153732..c94605129 100644 --- a/UI/Web/src/app/app.component.ts +++ b/UI/Web/src/app/app.component.ts @@ -13,7 +13,6 @@ import { SideNavComponent } from './sidenav/_components/side-nav/side-nav.compon import {NavHeaderComponent} from "./nav/_components/nav-header/nav-header.component"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {ServerService} from "./_services/server.service"; -import {ImportCblModalComponent} from "./reading-list/_modals/import-cbl-modal/import-cbl-modal.component"; import {OutOfDateModalComponent} from "./announcements/_components/out-of-date-modal/out-of-date-modal.component"; @Component({ @@ -101,9 +100,11 @@ export class AppComponent implements OnInit { // Bootstrap anything that's needed this.themeService.getThemes().subscribe(); this.libraryService.getLibraryNames().pipe(take(1), shareReplay({refCount: true, bufferSize: 1})).subscribe(); + this.libraryService.getLibraryTypes().pipe(take(1), shareReplay({refCount: true, bufferSize: 1})).subscribe(); // On load, make an initial call for valid license this.accountService.hasValidLicense().subscribe(); + // Every hour, have the UI check for an update. People seriously stay out of date interval(2* 60 * 60 * 1000) // 2 hours in milliseconds .pipe( diff --git a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts index 694f244cf..072ed5c20 100644 --- a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts +++ b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts @@ -634,12 +634,18 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.bookService.getBookInfo(this.chapterId).subscribe(info => { - if (this.readingListMode && info.seriesFormat !== MangaFormat.EPUB) { - // Redirect to the manga reader. - const params = this.readerService.getQueryParamsObject(this.incognitoMode, this.readingListMode, this.readingListId); - this.router.navigate(this.readerService.getNavigationArray(info.libraryId, info.seriesId, this.chapterId, info.seriesFormat), {queryParams: params}); - return; - } + this.libraryService.getLibraryType(this.libraryId).pipe(take(1)).subscribe(type => { + this.libraryType = type; + this.cdRef.markForCheck(); + + if (this.readingListMode && info.seriesFormat !== MangaFormat.EPUB) { + // Redirect to the manga reader. + const params = this.readerService.getQueryParamsObject(this.incognitoMode, this.readingListMode, this.readingListId); + this.router.navigate(this.readerService.getNavigationArray(info.libraryId, info.seriesId, this.chapterId, info.seriesFormat, this.libraryType), {queryParams: params}); + return; + } + }); + this.bookTitle = info.bookTitle; this.cdRef.markForCheck(); @@ -659,10 +665,6 @@ 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; - }); - this.updateImageSizes(); if (this.pageNum >= this.maxPages) { diff --git a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts index d4a4a85e6..466812132 100644 --- a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts +++ b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts @@ -263,7 +263,7 @@ export class CardDetailDrawerComponent implements OnInit { } const params = this.readerService.getQueryParamsObject(incognito, false); - this.router.navigate(this.readerService.getNavigationArray(this.libraryId, this.seriesId, chapter.id, chapter.files[0].format), {queryParams: params}); + this.router.navigate(this.readerService.getNavigationArray(this.libraryId, this.seriesId, chapter.id, chapter.files[0].format, this.libraryType), {queryParams: params}); this.close(); } diff --git a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts index 9fe413cb2..ec909846c 100644 --- a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts +++ b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts @@ -69,6 +69,7 @@ import {SwipeDirective} from '../../../ng-swipe/ng-swipe.directive'; import {LoadingComponent} from '../../../shared/loading/loading.component'; import {translate, TranslocoDirective} from "@ngneat/transloco"; import {shareReplay} from "rxjs/operators"; +import {LibraryService} from "../../../_services/library.service"; const PREFETCH_PAGES = 10; @@ -151,6 +152,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { private readonly cdRef = inject(ChangeDetectorRef); private readonly toastr = inject(ToastrService); public readonly readerService = inject(ReaderService); + public readonly libraryService = inject(LibraryService); public readonly utilityService = inject(UtilityService); public readonly mangaReaderService = inject(ManagaReaderService); @@ -411,7 +413,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { getPageUrl = (pageNum: number, chapterId: number = this.chapterId) => { if (this.bookmarkMode) return this.readerService.getBookmarkPageUrl(this.seriesId, this.user.apiKey, pageNum); - return this.readerService.getPageUrl(chapterId, pageNum); + return this.readerService.getPageUrl(chapterId, pageNum, this.libraryType == LibraryType.Magazine); } get CurrentPageBookmarked() { @@ -494,6 +496,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.incognitoMode = this.route.snapshot.queryParamMap.get('incognitoMode') === 'true'; this.bookmarkMode = this.route.snapshot.queryParamMap.get('bookmarkMode') === 'true'; + console.log('ngOnInit called: ', this.libraryId) + const readingListId = this.route.snapshot.queryParamMap.get('readingListId'); if (readingListId != null) { this.readingListMode = true; @@ -610,7 +614,12 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { }); }); - this.init(); + this.libraryService.getLibraryType(this.libraryId).subscribe(type => { + this.libraryType = type; + this.init(); + this.cdRef.markForCheck(); + }); + } ngAfterViewInit() { @@ -888,15 +897,29 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { return; } + // When readingListMode and we loaded from continuous reader, library/seriesId doesn't get refreshed. Need to reobtain from the chapterId + if (this.readingListMode) { + console.log('chapterId: ', this.chapterId) + this.cdRef.markForCheck(); + } + + // First load gets library type from constructor. Any other loads will get from continuous reader + console.log('library type', this.libraryType, 'mag: ', LibraryType.Magazine, 'equal: ', this.libraryType == LibraryType.Magazine); forkJoin({ progress: this.readerService.getProgress(this.chapterId), - chapterInfo: this.readerService.getChapterInfo(this.chapterId, true), + chapterInfo: this.readerService.getChapterInfo(this.chapterId, true, this.libraryType == LibraryType.Magazine), bookmarks: this.readerService.getBookmarks(this.chapterId), }).pipe(take(1)).subscribe(results => { if (this.readingListMode && (results.chapterInfo.seriesFormat === MangaFormat.EPUB || results.chapterInfo.seriesFormat === MangaFormat.PDF)) { // Redirect to the book reader. const params = this.readerService.getQueryParamsObject(this.incognitoMode, this.readingListMode, this.readingListId); - this.router.navigate(this.readerService.getNavigationArray(results.chapterInfo.libraryId, results.chapterInfo.seriesId, this.chapterId, results.chapterInfo.seriesFormat), {queryParams: params}); + if (results.chapterInfo.libraryType === LibraryType.Magazine) { + this.router.navigate(this.readerService.getNavigationArray(results.chapterInfo.libraryId, results.chapterInfo.seriesId, + this.chapterId, results.chapterInfo.seriesFormat, results.chapterInfo.libraryType), {queryParams: params}); + } else { + this.router.navigate(this.readerService.getNavigationArray(results.chapterInfo.libraryId, results.chapterInfo.seriesId, + this.chapterId, results.chapterInfo.seriesFormat, results.chapterInfo.libraryType), {queryParams: params}); + } return; } @@ -967,6 +990,13 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.closeReader(); }, 200); }); + + // LibraryId is wrong on readinglist mode + // this.libraryService.getLibraryType(this.libraryId).subscribe((type) => { + // this.libraryType = type; + // console.log('library type', this.libraryType, 'mag: ', LibraryType.Magazine, 'equal: ', this.libraryType == LibraryType.Magazine); + // + // }); } closeReader() { @@ -1300,6 +1330,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } loadChapter(chapterId: number, direction: 'Next' | 'Prev') { + if (chapterId > 0) { this.isLoading = true; this.cdRef.markForCheck(); @@ -1307,11 +1338,18 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.chapterId = chapterId; this.continuousChaptersStack.push(chapterId); // Load chapter Id onto route but don't reload - const newRoute = this.readerService.getNextChapterUrl(this.router.url, this.chapterId, this.incognitoMode, this.readingListMode, this.readingListId); - window.history.replaceState({}, '', newRoute); - this.init(); - const msg = translate(direction === 'Next' ? 'toasts.load-next-chapter' : 'toasts.load-prev-chapter', {entity: this.utilityService.formatChapterName(this.libraryType).toLowerCase()}); - this.toastr.info(msg, '', {timeOut: 3000}); + + + if (this.readingListMode) { + this.readerService.getChapterInfo(this.chapterId).subscribe(chapterInfo => { + this.libraryId = chapterInfo.libraryId; + this.seriesId = chapterInfo.seriesId; + this.libraryType = chapterInfo.libraryType; + this.#updatePageStateForNextChapter(direction); + }); + } else { + this.#updatePageStateForNextChapter(direction); + } } else { // This will only happen if no actual chapter can be found const msg = translate(direction === 'Next' ? 'toasts.no-next-chapter' : 'toasts.no-prev-chapter', @@ -1327,6 +1365,14 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } } + #updatePageStateForNextChapter(direction: 'Next' | 'Prev') { + const newRoute = this.readerService.getNextChapterUrl(this.router.url, this.chapterId, this.incognitoMode, this.readingListMode, this.readingListId); + window.history.replaceState({}, '', newRoute); + this.init(); + const msg = translate(direction === 'Next' ? 'toasts.load-next-chapter' : 'toasts.load-prev-chapter', {entity: this.utilityService.formatChapterName(this.libraryType).toLowerCase()}); + this.toastr.info(msg, '', {timeOut: 3000}); + } + renderPage() { @@ -1434,7 +1480,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { if (this.pageNum >= this.maxPages - 10) { // Tell server to cache the next chapter if (this.nextChapterId > 0 && !this.nextChapterPrefetched) { - this.readerService.getChapterInfo(this.nextChapterId).pipe(take(1)).subscribe(res => { + this.readerService.getChapterInfo(this.nextChapterId, false, this.libraryType == LibraryType.Magazine).pipe(take(1)).subscribe(res => { this.continuousChapterInfos[ChapterInfoPosition.Next] = res; this.nextChapterPrefetched = true; this.prefetchStartOfChapter(this.nextChapterId, PAGING_DIRECTION.FORWARD); @@ -1442,7 +1488,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } } else if (this.pageNum <= 10) { if (this.prevChapterId > 0 && !this.prevChapterPrefetched) { - this.readerService.getChapterInfo(this.prevChapterId).pipe(take(1)).subscribe(res => { + this.readerService.getChapterInfo(this.prevChapterId, false, this.libraryType == LibraryType.Magazine).pipe(take(1)).subscribe(res => { this.continuousChapterInfos[ChapterInfoPosition.Previous] = res; this.prevChapterPrefetched = true; this.prefetchStartOfChapter(this.nextChapterId, PAGING_DIRECTION.BACKWARDS); diff --git a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts index fa9073380..5281be81d 100644 --- a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts +++ b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts @@ -145,7 +145,8 @@ export class ReadingListDetailComponent implements OnInit { readChapter(item: ReadingListItem) { if (!this.readingList) return; const params = this.readerService.getQueryParamsObject(false, true, this.readingList.id); - this.router.navigate(this.readerService.getNavigationArray(item.libraryId, item.seriesId, item.chapterId, item.seriesFormat), {queryParams: params}); + this.router.navigate(this.readerService.getNavigationArray(item.libraryId, item.seriesId, item.chapterId, + item.seriesFormat, item.libraryType), {queryParams: params}); } async handleReadingListActionCallback(action: ActionItem, readingList: ReadingList) { @@ -209,7 +210,8 @@ export class ReadingListDetailComponent implements OnInit { if (!this.readingList) return; const firstItem = this.items[0]; this.router.navigate( - this.readerService.getNavigationArray(firstItem.libraryId, firstItem.seriesId, firstItem.chapterId, firstItem.seriesFormat), + this.readerService.getNavigationArray(firstItem.libraryId, firstItem.seriesId, firstItem.chapterId, + firstItem.seriesFormat, firstItem.libraryType), {queryParams: {readingListId: this.readingList.id, incognitoMode: incognitoMode}}); } @@ -226,7 +228,8 @@ export class ReadingListDetailComponent implements OnInit { } this.router.navigate( - this.readerService.getNavigationArray(currentlyReadingChapter.libraryId, currentlyReadingChapter.seriesId, currentlyReadingChapter.chapterId, currentlyReadingChapter.seriesFormat), + this.readerService.getNavigationArray(currentlyReadingChapter.libraryId, currentlyReadingChapter.seriesId, + currentlyReadingChapter.chapterId, currentlyReadingChapter.seriesFormat, currentlyReadingChapter.libraryType), {queryParams: {readingListId: this.readingList.id, incognitoMode: incognitoMode}}); } diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts index 5071d7b0e..bb14a3f1c 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts @@ -719,7 +719,11 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { // Book libraries only have Volumes or Specials enabled if (this.libraryType === LibraryType.Magazine) { if (this.volumes.length === 0) { - this.activeTabId = TabID.Chapters; + if (this.chapters.length > 0) { + this.activeTabId = TabID.Chapters; + } else { + this.activeTabId = TabID.Specials; + } } else { this.activeTabId = TabID.Volumes; } @@ -840,7 +844,8 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { this.toastr.error(this.translocoService.translate('series-detail.no-pages')); return; } - this.router.navigate(this.readerService.getNavigationArray(this.libraryId, this.seriesId, chapter.id, chapter.files[0].format), {queryParams: {incognitoMode}}); + this.router.navigate(this.readerService.getNavigationArray(this.libraryId, this.seriesId, chapter.id, + chapter.files[0].format, this.libraryType), {queryParams: {incognitoMode}}); } openVolume(volume: Volume) { diff --git a/UI/Web/src/app/statistics/_components/reading-activity/reading-activity.component.html b/UI/Web/src/app/statistics/_components/reading-activity/reading-activity.component.html index a2ef443a8..53c16644b 100644 --- a/UI/Web/src/app/statistics/_components/reading-activity/reading-activity.component.html +++ b/UI/Web/src/app/statistics/_components/reading-activity/reading-activity.component.html @@ -11,7 +11,7 @@ diff --git a/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.html b/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.html index cd90281d5..d92afae6c 100644 --- a/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.html +++ b/UI/Web/src/app/statistics/_components/server-stats/server-stats.component.html @@ -4,7 +4,7 @@
- {{stats.seriesCount | compactNumber}} Series + {{t('count-series', {num: stats.seriesCount | number})}}
@@ -13,7 +13,7 @@
- {{stats.volumeCount | compactNumber}} Volumes + {{t('count-volume', {num: stats.volumeCount | number})}}
@@ -22,7 +22,7 @@
- {{stats.totalFiles | compactNumber}} Files + {{t('file-volume', {num: stats.totalFiles | number})}}
@@ -88,7 +88,7 @@
- +
diff --git a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts index a57fef24c..8f8a568db 100644 --- a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts +++ b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts @@ -166,7 +166,6 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { this.route.fragment.subscribe(frag => { const tab = this.tabs.filter(item => item.fragment === frag); - console.log('tab: ', tab); if (tab.length > 0) { this.active = tab[0]; } else { diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index a8f6ae582..d2404c395 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -1757,6 +1757,7 @@ "y-axis-label": "Hours Read", "no-data": "No Reading Progress", "time-frame-label": "Time Frame", + "all-users": "All Users", "this-week": "{{time-periods.this-week}}", "last-7-days": "{{time-periods.last-7-days}}", "last-30-days": "{{time-periods.last-30-days}}", @@ -1819,6 +1820,9 @@ "popular-libraries-title": "Popular Libraries", "popular-series-title": "Popular Series", "recently-read-title": "Recently Read", + "series-count": "{{num}} Series", + "volume-count": "{{num}} Volumes", + "file-count": "{{num}} Files", "genre-count": "{{num}} Genres", "tag-count": "{{num}} Tags", "people-count": "{{num}} People", diff --git a/openapi.json b/openapi.json index 0dbb8432a..0510eb1b8 100644 --- a/openapi.json +++ b/openapi.json @@ -2949,6 +2949,45 @@ } } }, + "/api/Library/types": { + "get": { + "tags": [ + "Library" + ], + "summary": "Return pairs of all types", + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LibraryTypeDto" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LibraryTypeDto" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LibraryTypeDto" + } + } + } + } + } + } + } + }, "/api/License/valid-license": { "get": { "tags": [ @@ -13458,6 +13497,18 @@ "chapterTitle": { "type": "string", "nullable": true + }, + "libraryType": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "type": "integer", + "format": "int32" } }, "additionalProperties": false @@ -16145,6 +16196,29 @@ }, "additionalProperties": false }, + "LibraryTypeDto": { + "type": "object", + "properties": { + "libraryId": { + "type": "integer", + "format": "int32" + }, + "libraryType": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false, + "description": "Simple pairing of LibraryId and LibraryType" + }, "LoginDto": { "type": "object", "properties": { From bad5c9dcd6dabb7b1f9c7e16088318fd8cc9b57f Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sun, 4 May 2025 10:00:20 -0500 Subject: [PATCH 5/6] Fixed up some logic around the magazine to undo a previous direction --- API/DTOs/LibraryTypeDto.cs | 2 +- API/Services/Tasks/Scanner/Parser/MagazineParser.cs | 5 +++-- .../_components/book-reader/book-reader.component.ts | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/API/DTOs/LibraryTypeDto.cs b/API/DTOs/LibraryTypeDto.cs index 1f23c604b..9f448e7b7 100644 --- a/API/DTOs/LibraryTypeDto.cs +++ b/API/DTOs/LibraryTypeDto.cs @@ -5,7 +5,7 @@ namespace API.DTOs; /// /// Simple pairing of LibraryId and LibraryType /// -public class LibraryTypeDto +public sealed record LibraryTypeDto { public int LibraryId { get; set; } public LibraryType LibraryType { get; set; } diff --git a/API/Services/Tasks/Scanner/Parser/MagazineParser.cs b/API/Services/Tasks/Scanner/Parser/MagazineParser.cs index 886ec33c9..50bbfd403 100644 --- a/API/Services/Tasks/Scanner/Parser/MagazineParser.cs +++ b/API/Services/Tasks/Scanner/Parser/MagazineParser.cs @@ -4,6 +4,7 @@ using API.Data.Metadata; using API.Entities.Enums; namespace API.Services.Tasks.Scanner.Parser; +#nullable enable public class MagazineParser(IDirectoryService directoryService) : DefaultParser(directoryService) { @@ -17,7 +18,7 @@ public class MagazineParser(IDirectoryService directoryService) : DefaultParser( Volumes = Parser.LooseLeafVolume, Chapters = Parser.DefaultChapter, ComicInfo = comicInfo, - Format = MangaFormat.Image, + Format = Parser.ParseFormat(filePath), Filename = Path.GetFileName(filePath), FullFilePath = Parser.NormalizePath(filePath), Series = string.Empty, @@ -71,7 +72,7 @@ public class MagazineParser(IDirectoryService directoryService) : DefaultParser( } /// - /// Only applicable for Image files and Image library type + /// Only applicable for PDF Files and Magazine library type /// /// /// diff --git a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts index 3c65d0668..77b88d9db 100644 --- a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts +++ b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts @@ -647,6 +647,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.bookService.getBookInfo(this.chapterId).subscribe(info => { + this.libraryService.getLibraryType(this.libraryId).pipe(take(1)).subscribe(type => { this.libraryType = type; this.cdRef.markForCheck(); From 29167f281ecd3ee4b391c9dd05f68016fff07fc1 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sun, 4 May 2025 11:53:42 -0500 Subject: [PATCH 6/6] After reviewing, this needs some major work to finish it off. --- API.Tests/Parsing/MagazineParserTests.cs | 12 ++++---- API/Services/SeriesService.cs | 11 ++----- .../Tasks/Scanner/Parser/MagazineParser.cs | 1 + API/Services/Tasks/Scanner/Parser/Parser.cs | 30 +++++++++++++++++++ 4 files changed, 40 insertions(+), 14 deletions(-) diff --git a/API.Tests/Parsing/MagazineParserTests.cs b/API.Tests/Parsing/MagazineParserTests.cs index 53735632e..f6e71d9e0 100644 --- a/API.Tests/Parsing/MagazineParserTests.cs +++ b/API.Tests/Parsing/MagazineParserTests.cs @@ -34,10 +34,10 @@ public class MagazineParserTests Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseMagazineChapter(filename)); } - // [Theory] - // [InlineData("AIR International Vol. 14 No. 3 (ISSN 1011-3250)", "1011-3250")] - // public void ParseGTINTest(string filename, string expected) - // { - // Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseGTIN(filename)); - // } + [Theory] + [InlineData("AIR International Vol. 14 No. 3 (ISSN 1011-3250)", "1011-3250")] + public void ParseGTINTest(string filename, string expected) + { + Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseGTIN(filename)); + } } diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index 6ac5e0ecb..9b06f7e51 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -33,7 +33,6 @@ public interface ISeriesService Task UpdateRelatedSeries(UpdateRelatedSeriesDto dto); Task GetRelatedSeries(int userId, int seriesId); Task FormatChapterTitle(int userId, ChapterDto chapter, LibraryType libraryType, bool withHash = true); - Task FormatChapterTitle(int userId, Chapter chapter, LibraryType libraryType, bool withHash = true); Task FormatChapterTitle(int userId, bool isSpecial, LibraryType libraryType, string chapterRange, string? chapterTitle, bool withHash); Task FormatChapterName(int userId, LibraryType libraryType, bool withHash = false); @@ -633,7 +632,7 @@ public class SeriesService : ISeriesService public async Task FormatChapterTitle(int userId, bool isSpecial, LibraryType libraryType, string chapterRange, string? chapterTitle, bool withHash) { - if (string.IsNullOrEmpty(chapterTitle) && (isSpecial || libraryType == LibraryType.Book)) throw new ArgumentException("Chapter Title cannot be null"); + if (string.IsNullOrEmpty(chapterTitle) && (isSpecial || (libraryType == LibraryType.Book || libraryType == LibraryType.Magazine))) throw new ArgumentException("Chapter Title cannot be null"); if (isSpecial) { @@ -643,10 +642,10 @@ public class SeriesService : ISeriesService var hashSpot = withHash ? "#" : string.Empty; var baseChapter = libraryType switch { - LibraryType.Book => await _localizationService.Translate(userId, "book-num", chapterTitle!), + LibraryType.Book => await _localizationService.Translate(userId, "book-num", chapterTitle ?? string.Empty), LibraryType.LightNovel => await _localizationService.Translate(userId, "book-num", chapterRange), LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", hashSpot, chapterRange), - LibraryType.Magazine => await _localizationService.Translate(userId, "issue-num", hashSpot, chapterTitle), + LibraryType.Magazine => await _localizationService.Translate(userId, "issue-num", hashSpot, chapterTitle ?? string.Empty), LibraryType.ComicVine => await _localizationService.Translate(userId, "issue-num", hashSpot, chapterRange), LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", chapterRange), LibraryType.Image => await _localizationService.Translate(userId, "chapter-num", chapterRange), @@ -667,10 +666,6 @@ public class SeriesService : ISeriesService return await FormatChapterTitle(userId, chapter.IsSpecial, libraryType, chapter.Range, chapter.Title, withHash); } - public async Task FormatChapterTitle(int userId, Chapter chapter, LibraryType libraryType, bool withHash = true) - { - return await FormatChapterTitle(userId, chapter.IsSpecial, libraryType, chapter.Range, chapter.Title, withHash); - } // TODO: Refactor this out and use FormatChapterTitle instead across library public async Task FormatChapterName(int userId, LibraryType libraryType, bool withHash = false) diff --git a/API/Services/Tasks/Scanner/Parser/MagazineParser.cs b/API/Services/Tasks/Scanner/Parser/MagazineParser.cs index 50bbfd403..1aa0ff19d 100644 --- a/API/Services/Tasks/Scanner/Parser/MagazineParser.cs +++ b/API/Services/Tasks/Scanner/Parser/MagazineParser.cs @@ -42,6 +42,7 @@ public class MagazineParser(IDirectoryService directoryService) : DefaultParser( { ret.Series = Parser.CleanTitle(folders[^1]); } + var hasGeoCode = !string.IsNullOrEmpty(Parser.ParseGeoCode(ret.Series)); foreach (var folder in folders[..^1]) { diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index 18330089c..84b723fa9 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -958,6 +958,36 @@ public static partial class Parser } + // /// + // /// Tries to parse a GTIN/ISBN out of a string + // /// + // /// + // /// + // public static string? ParseGTIN(string? value) + // { + // if (string.IsNullOrEmpty(value)) return value; + // const string pattern = @"\b(?:\(|\[|\{)([A-Z]{2})(?:\)|\]|\})\b|^([A-Z]{2})$"; + // + // // Match the pattern in the input string + // var match = Regex.Match(value, pattern, RegexOptions.IgnoreCase); + // + // if (match.Success) + // { + // // Extract the GeoCode from the first capturing group if it exists, + // // otherwise, extract the GeoCode from the second capturing group + // var extractedCode = match.Groups[1].Success ? match.Groups[1].Value : match.Groups[2].Value; + // + // // Validate the extracted GeoCode against the list of valid GeoCodes + // if (GeoCodes.Contains(extractedCode)) + // { + // return extractedCode; + // } + // } + // + // return null; + // } + + private static string FormatValue(string value, bool hasPart) { if (!value.Contains('-'))