Compare commits

...
Sign in to create a new pull request.

9 commits

Author SHA1 Message Date
Joseph Milazzo
29167f281e After reviewing, this needs some major work to finish it off. 2025-05-04 11:53:42 -05:00
Joseph Milazzo
bad5c9dcd6 Fixed up some logic around the magazine to undo a previous direction 2025-05-04 10:00:20 -05:00
Joseph Milazzo
08a32a26bc Merge branch 'develop' of https://github.com/Kareadita/Kavita into feature/magazine 2025-05-04 09:20:15 -05:00
Joseph Milazzo
d12a79892f Merged develop in 2025-04-26 16:17:05 -05:00
Joseph Milazzo
b3f6a574cd 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.
2024-02-11 14:30:13 -06:00
Joseph Milazzo
95e7ad0f5b Basic fallback parsing code is in place. 2024-02-10 16:39:37 -06:00
Joseph Milazzo
5a522b6d5b Setup the parsing rules for Magazines. 2024-02-10 13:10:06 -06:00
Joseph Milazzo
a443be7523 Merged develop in 2024-02-10 09:53:14 -06:00
Joseph Milazzo
93df0def48 Started working on the magazine library type. Taking a break as I need some more community feedback. 2024-01-08 13:58:08 -06:00
25 changed files with 470 additions and 39 deletions

View file

@ -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));
}
}

View file

@ -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,
});

View file

@ -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));
}
/// <summary>
/// Return pairs of all types
/// </summary>
/// <returns></returns>
[HttpGet("types")]
public async Task<ActionResult<IEnumerable<LibraryTypeDto>>> GetLibraryTypes()
{
return Ok(await _unitOfWork.LibraryRepository.GetLibraryTypesAsync(User.GetUserId()));
}
}

View file

@ -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);
}
/// <summary>
/// Returns various information about all bookmark files for a Series. Side effect: This will cache the bookmark images for reading.
/// </summary>

View file

@ -0,0 +1,12 @@
using API.Entities.Enums;
namespace API.DTOs;
/// <summary>
/// Simple pairing of LibraryId and LibraryType
/// </summary>
public sealed record LibraryTypeDto
{
public int LibraryId { get; set; }
public LibraryType LibraryType { get; set; }
}

View file

@ -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; }
}

View file

@ -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; }
}

View file

@ -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;
}

View file

@ -58,6 +58,7 @@ public interface ILibraryRepository
Task<bool> GetAllowsScrobblingBySeriesId(int seriesId);
Task<IDictionary<int, LibraryType>> GetLibraryTypesBySeriesIdsAsync(IList<int> seriesIds);
Task<IEnumerable<LibraryTypeDto>> 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<IEnumerable<LibraryTypeDto>> 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();
}
}

View file

@ -34,4 +34,9 @@ public enum LibraryType
/// </summary>
[Description("Comic")]
ComicVine = 5,
/// <summary>
/// Uses Magazine regex and is restricted to PDF and Archive by default
/// </summary>
[Description("Magazine")]
Magazine = 6
}

View file

@ -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<ReadingItemService> 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));

View file

@ -33,7 +33,6 @@ public interface ISeriesService
Task<bool> UpdateRelatedSeries(UpdateRelatedSeriesDto dto);
Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId);
Task<string> FormatChapterTitle(int userId, ChapterDto chapter, LibraryType libraryType, bool withHash = true);
Task<string> FormatChapterTitle(int userId, Chapter chapter, LibraryType libraryType, bool withHash = true);
Task<string> FormatChapterTitle(int userId, bool isSpecial, LibraryType libraryType, string chapterRange, string? chapterTitle,
bool withHash);
Task<string> FormatChapterName(int userId, LibraryType libraryType, bool withHash = false);
@ -633,7 +632,7 @@ public class SeriesService : ISeriesService
public async Task<string> 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<string> 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<string> FormatChapterName(int userId, LibraryType libraryType, bool withHash = false)

View file

@ -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);
}
}
}

View file

@ -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;
}
/// <summary>
/// Only applicable for PDF Files and Magazine library type
/// </summary>
/// <param name="filePath"></param>
/// <param name="type"></param>
/// <returns></returns>
public override bool IsApplicable(string filePath, LibraryType type)
{
return type == LibraryType.Magazine && Parser.IsPdf(filePath);
}
}

View file

@ -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
/// </summary>
private const string TagsInBrackets = $@"\[(?!\s){BalancedBracket}(?<!\s)\]";
[GeneratedRegex(@"^\d+$")]
private static partial Regex IsNumberRegex();
/// <summary>
/// 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<string> GeoCodes = new(CreateCountryCodes());
private static readonly Dictionary<string, int> MonthMappings = CreateMonthMappings();
private static readonly Regex[] MagazineSeriesRegex =
[
// 3D World - 2018 UK, 3D World - 022014
new Regex(
@"^(?<Series>.+?)(_|\s)*-(_|\s)*\d{4,6}.*",
MatchOptions, RegexTimeout),
// AIR International - April 2018 UK
new Regex(
@"^(?<Series>.+?)(_|\s)*-(_|\s)*.*",
MatchOptions, RegexTimeout),
// AIR International #1 // This breaks the way the code works
// new Regex(
// @"^(?<Series>.+?)(_|\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(
@"^(?<Series>.+?)(_|\s)*-(_|\s)*\d{2}?(?<Volume>\d{4}).*",
MatchOptions, RegexTimeout),
// 3D World - Sept 2018
new Regex(
@"^(?<Series>.+?)(_|\s)*-(_|\s)*\D+(?<Volume>\d{4}).*",
MatchOptions, RegexTimeout),
// 3D World - Sept 2018
new Regex(
@"^(?<Series>.+?)(_|\s)*-(_|\s)*\D+(?<Volume>\d{4}).*",
MatchOptions, RegexTimeout),
};
private static readonly Regex[] MagazineChapterRegex = new[]
{
// 3D World - September 2023 #2
new Regex(
@"^(?<Series>.+?)(_|\s)*-(_|\s)*.*#(?<Chapter>\d+).*",
MatchOptions, RegexTimeout),
// Computer Weekly - September 2023
new Regex(
@"^(?<Series>.+?)(_|\s)*-(_|\s)*(?<Chapter>January|February|March|April|May|June|July|August|September|October|November|December).*",
MatchOptions, RegexTimeout),
// Computer Weekly - Sept 2023
new Regex(
@"^(?<Series>.+?)(_|\s)*-(_|\s)*(?<Chapter>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<string, int> CreateMonthMappings()
{
Dictionary<string, int> 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;
}
/// <summary>
/// Tries to parse a GeoCode (UK, US) out of a string
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
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;
}
// /// <summary>
// /// Tries to parse a GTIN/ISBN out of a string
// /// </summary>
// /// <param name="value"></param>
// /// <returns></returns>
// 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)
/// <summary>
/// Extracts year from Series (Year)
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
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)

View file

@ -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;

View file

@ -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 '';
}

View file

@ -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<Array<{libraryId: number, libraryType: LibraryType}>>(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]);

View file

@ -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) {

View file

@ -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: 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}});
}

View file

@ -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');
}

View file

@ -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
}
}

View file

@ -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);

View file

@ -583,7 +583,8 @@
"manga": "Manga",
"comicVine": "Comic",
"image": "Image",
"lightNovel": "Light Novel"
"lightNovel": "Light Novel",
"magazine": "Magazine"
},
"age-rating-pipe": {