diff --git a/API.Tests/Parsing/MagazineParserTests.cs b/API.Tests/Parsing/MagazineParserTests.cs
new file mode 100644
index 000000000..f6e71d9e0
--- /dev/null
+++ b/API.Tests/Parsing/MagazineParserTests.cs
@@ -0,0 +1,43 @@
+using Xunit;
+
+namespace API.Tests.Parser;
+
+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("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/Controllers/BookController.cs b/API/Controllers/BookController.cs
index e1d7da9e8..251811346 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 2f12aa1fe..dbd809cee 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;
@@ -654,4 +655,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 38a5ad482..207dbabb5 100644
--- a/API/Controllers/ReaderController.cs
+++ b/API/Controllers/ReaderController.cs
@@ -246,6 +246,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,
@@ -287,6 +288,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..9f448e7b7
--- /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 sealed record 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 2473cd5dc..5c4e530c6 100644
--- a/API/DTOs/Reader/BookInfoDto.cs
+++ b/API/DTOs/Reader/BookInfoDto.cs
@@ -15,4 +15,5 @@ public sealed record 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 27d21df74..ce7c44baa 100644
--- a/API/Data/Repositories/ChapterRepository.cs
+++ b/API/Data/Repositories/ChapterRepository.cs
@@ -137,11 +137,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 78022fa9a..b3f905dce 100644
--- a/API/Data/Repositories/LibraryRepository.cs
+++ b/API/Data/Repositories/LibraryRepository.cs
@@ -58,6 +58,7 @@ public interface ILibraryRepository
Task GetAllowsScrobblingBySeriesId(int seriesId);
Task> GetLibraryTypesBySeriesIdsAsync(IList seriesIds);
+ Task> GetLibraryTypesAsync(int userId);
}
public class LibraryRepository : ILibraryRepository
@@ -369,4 +370,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/Entities/Enums/LibraryType.cs b/API/Entities/Enums/LibraryType.cs
index a8d943b2d..b79315803 100644
--- a/API/Entities/Enums/LibraryType.cs
+++ b/API/Entities/Enums/LibraryType.cs
@@ -34,4 +34,9 @@ public enum LibraryType
///
[Description("Comic")]
ComicVine = 5,
+ ///
+ /// Uses Magazine regex and is restricted to PDF and Archive by default
+ ///
+ [Description("Magazine")]
+ Magazine = 6
}
diff --git a/API/Services/ReadingItemService.cs b/API/Services/ReadingItemService.cs
index efdaec8ff..464c3b33c 100644
--- a/API/Services/ReadingItemService.cs
+++ b/API/Services/ReadingItemService.cs
@@ -27,6 +27,7 @@ public class ReadingItemService : IReadingItemService
private readonly ImageParser _imageParser;
private readonly BookParser _bookParser;
private readonly PdfParser _pdfParser;
+ private readonly MagazineParser _magazineParser;
public ReadingItemService(IArchiveService archiveService, IBookService bookService, IImageService imageService,
IDirectoryService directoryService, ILogger logger)
@@ -42,6 +43,7 @@ public class ReadingItemService : IReadingItemService
_bookParser = new BookParser(directoryService, bookService, _basicParser);
_comicVineParser = new ComicVineParser(directoryService);
_pdfParser = new PdfParser(directoryService);
+ _magazineParser = new MagazineParser(directoryService);
}
@@ -189,6 +191,10 @@ public class ReadingItemService : IReadingItemService
{
return _bookParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path));
}
+ if (_magazineParser.IsApplicable(path, type))
+ {
+ return _magazineParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path));
+ }
if (_pdfParser.IsApplicable(path, type))
{
return _pdfParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path));
diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs
index 805b3b06f..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,9 +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 ?? 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),
@@ -666,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/ComicVineParser.cs b/API/Services/Tasks/Scanner/Parser/ComicVineParser.cs
index b68596245..f632bcd59 100644
--- a/API/Services/Tasks/Scanner/Parser/ComicVineParser.cs
+++ b/API/Services/Tasks/Scanner/Parser/ComicVineParser.cs
@@ -57,7 +57,7 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser
{
if (!Parser.IsSeriesAndYear(directory)) continue;
info.Series = directory;
- info.Volumes = Parser.ParseYear(directory);
+ info.Volumes = Parser.ParseYearFromSeries(directory);
break;
}
@@ -72,7 +72,7 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser
if (Parser.IsSeriesAndYear(directoryName))
{
info.Series = directoryName;
- info.Volumes = Parser.ParseYear(directoryName);
+ info.Volumes = Parser.ParseYearFromSeries(directoryName);
}
}
}
diff --git a/API/Services/Tasks/Scanner/Parser/MagazineParser.cs b/API/Services/Tasks/Scanner/Parser/MagazineParser.cs
new file mode 100644
index 000000000..1aa0ff19d
--- /dev/null
+++ b/API/Services/Tasks/Scanner/Parser/MagazineParser.cs
@@ -0,0 +1,86 @@
+using System.IO;
+using System.Linq;
+using API.Data.Metadata;
+using API.Entities.Enums;
+
+namespace API.Services.Tasks.Scanner.Parser;
+#nullable enable
+
+public class MagazineParser(IDirectoryService directoryService) : DefaultParser(directoryService)
+{
+ public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type,
+ ComicInfo? comicInfo = null)
+ {
+ if (!IsApplicable(filePath, type)) return null;
+
+ var ret = new ParserInfo
+ {
+ Volumes = Parser.LooseLeafVolume,
+ Chapters = Parser.DefaultChapter,
+ ComicInfo = comicInfo,
+ Format = Parser.ParseFormat(filePath),
+ Filename = Path.GetFileName(filePath),
+ FullFilePath = Parser.NormalizePath(filePath),
+ Series = string.Empty,
+ };
+
+ // Try to parse Series from the filename
+ var libraryPath = directoryService.FileSystem.DirectoryInfo.New(rootPath).Parent?.FullName ?? rootPath;
+ 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).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.LooseLeafVolume)
+ {
+ var vol = Parser.ParseYear(folder); // TODO: This might be better as YearFromSeries
+ 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;
+ }
+ }
+
+ }
+ }
+
+ return ret;
+ }
+
+ ///
+ /// Only applicable for PDF Files and Magazine library type
+ ///
+ ///
+ ///
+ ///
+ public override bool IsApplicable(string filePath, LibraryType type)
+ {
+ return type == LibraryType.Magazine && Parser.IsPdf(filePath);
+ }
+
+}
diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs
index 12987b18b..84b723fa9 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;
@@ -64,6 +66,8 @@ public static partial class Parser
///
private const string TagsInBrackets = $@"\[(?!\s){BalancedBracket}(?
/// Matches against font-family css syntax. Does not match if url import has data: starting, as that is binary data
@@ -667,6 +671,69 @@ public static partial class Parser
MatchOptions, RegexTimeout
);
+ #region Magazine
+
+ 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}.*",
+ MatchOptions, RegexTimeout),
+ // AIR International - April 2018 UK
+ 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[]
+ {
+ // 3D World - 2018 UK, 3D World - 022014
+ new Regex(
+ @"^(?.+?)(_|\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),
+ };
+
+ private static readonly Regex YearRegex = new(
+ @"(\b|\s|_)[1-9]{1}\d{3}(\b|\s|_)",
+ MatchOptions, RegexTimeout
+ );
+
+ #endregion
+
public static MangaFormat ParseFormat(string filePath)
@@ -739,6 +806,20 @@ public static partial 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 ParseMangaVolume(string filename)
{
foreach (var regex in MangaVolumeRegex)
@@ -776,6 +857,137 @@ public static partial class Parser
}
+ 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;
+ return FormatValue(value, false);
+ }
+ }
+
+ return LooseLeafVolume;
+ }
+
+ 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()
+ {
+ 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
+
+ return mappings;
+ }
+
+ 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;
+ }
+
+ ///
+ /// 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;
+ }
+
+
+ // ///
+ // /// 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('-'))
@@ -1159,13 +1371,21 @@ public static partial class Parser
return !string.IsNullOrEmpty(name) && SeriesAndYearRegex.IsMatch(name);
}
- public static string ParseYear(string? name)
+ ///
+ /// Extracts year from Series (Year)
+ ///
+ ///
+ ///
+ public static string ParseYearFromSeries(string? name)
{
if (string.IsNullOrEmpty(name)) return string.Empty;
var match = SeriesAndYearRegex.Match(name);
- if (!match.Success) return string.Empty;
+ return !match.Success ? string.Empty : match.Groups["Year"].Value;
+ }
- return match.Groups["Year"].Value;
+ public static string ParseYear(string? value)
+ {
+ return string.IsNullOrEmpty(value) ? string.Empty : YearRegex.Match(value).Value;
}
public static string? RemoveExtensionIfSupported(string? filename)
diff --git a/UI/Web/src/app/_models/library/library.ts b/UI/Web/src/app/_models/library/library.ts
index 74cabc658..000723568 100644
--- a/UI/Web/src/app/_models/library/library.ts
+++ b/UI/Web/src/app/_models/library/library.ts
@@ -6,10 +6,11 @@ export enum LibraryType {
Book = 2,
Images = 3,
LightNovel = 4,
- ComicVine = 5
+ ComicVine = 5,
+ Magazine = 6
}
-export const allLibraryTypes = [LibraryType.Manga, LibraryType.ComicVine, LibraryType.Comic, LibraryType.Book, LibraryType.LightNovel, LibraryType.Images];
+export const allLibraryTypes = [LibraryType.Manga, LibraryType.ComicVine, LibraryType.Comic, LibraryType.Book, LibraryType.LightNovel, LibraryType.Images, LibraryType.Magazine];
export interface Library {
id: number;
diff --git a/UI/Web/src/app/_pipes/library-type.pipe.ts b/UI/Web/src/app/_pipes/library-type.pipe.ts
index 1881b64d5..784089e6e 100644
--- a/UI/Web/src/app/_pipes/library-type.pipe.ts
+++ b/UI/Web/src/app/_pipes/library-type.pipe.ts
@@ -26,6 +26,8 @@ export class LibraryTypePipe implements PipeTransform {
return this.translocoService.translate('library-type-pipe.manga');
case LibraryType.LightNovel:
return this.translocoService.translate('library-type-pipe.lightNovel');
+ case LibraryType.Magazine:
+ return this.translocoService.translate('library-type-pipe.magazine');
default:
return '';
}
diff --git a/UI/Web/src/app/_services/library.service.ts b/UI/Web/src/app/_services/library.service.ts
index 8c851dd80..9178cd137 100644
--- a/UI/Web/src/app/_services/library.service.ts
+++ b/UI/Web/src/app/_services/library.service.ts
@@ -136,6 +136,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/book-reader/_components/book-reader/book-reader.component.ts b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts
index 6abd619f8..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,12 +647,19 @@ 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), {queryParams: params});
+ return;
+ }
+ });
+
this.bookTitle = info.bookTitle;
this.cdRef.markForCheck();
@@ -672,10 +679,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
new file mode 100644
index 000000000..e69de29bb
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 64a933552..2a65f1b01 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
@@ -282,7 +282,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), {queryParams: params});
}
async handleReadingListActionCallback(action: ActionItem, readingList: ReadingList) {
@@ -366,7 +367,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),
{queryParams: {readingListId: this.readingList.id, incognitoMode: incognitoMode}});
}
@@ -383,7 +385,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),
{queryParams: {readingListId: this.readingList.id, incognitoMode: incognitoMode}});
}
diff --git a/UI/Web/src/app/shared/_services/utility.service.ts b/UI/Web/src/app/shared/_services/utility.service.ts
index 94c5cf696..a2edc3c5a 100644
--- a/UI/Web/src/app/shared/_services/utility.service.ts
+++ b/UI/Web/src/app/shared/_services/utility.service.ts
@@ -1,12 +1,12 @@
-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 {translate, TranslocoService} from "@jsverse/transloco";
+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 {translate} from "@jsverse/transloco";
import {debounceTime, ReplaySubject, shareReplay} from "rxjs";
export enum KEY_CODES {
@@ -73,6 +73,7 @@ export class UtilityService {
return translate('common.book-num' + extra) + (includeSpace ? ' ' : '');
case LibraryType.Comic:
case LibraryType.ComicVine:
+ case LibraryType.Magazine:
if (includeHash) {
return translate('common.issue-hash-num');
}
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 9ac7df15c..4c68d6d3e 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
@@ -212,6 +212,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 d8a0ff752..9785ddbc4 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
@@ -214,6 +214,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;
}
this.libraryForm.get('allowScrobbling')?.setValue(this.IsKavitaPlusEligible);
diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json
index e460e3ffa..17319c120 100644
--- a/UI/Web/src/assets/langs/en.json
+++ b/UI/Web/src/assets/langs/en.json
@@ -583,7 +583,8 @@
"manga": "Manga",
"comicVine": "Comic",
"image": "Image",
- "lightNovel": "Light Novel"
+ "lightNovel": "Light Novel",
+ "magazine": "Magazine"
},
"age-rating-pipe": {