Compare commits
9 commits
develop
...
feature/ma
Author | SHA1 | Date | |
---|---|---|---|
![]() |
29167f281e | ||
![]() |
bad5c9dcd6 | ||
![]() |
08a32a26bc | ||
![]() |
d12a79892f | ||
![]() |
b3f6a574cd | ||
![]() |
95e7ad0f5b | ||
![]() |
5a522b6d5b | ||
![]() |
a443be7523 | ||
![]() |
93df0def48 |
25 changed files with 470 additions and 39 deletions
43
API.Tests/Parsing/MagazineParserTests.cs
Normal file
43
API.Tests/Parsing/MagazineParserTests.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -82,6 +82,7 @@ public class BookController : BaseApiController
|
||||||
SeriesFormat = dto.SeriesFormat,
|
SeriesFormat = dto.SeriesFormat,
|
||||||
SeriesId = dto.SeriesId,
|
SeriesId = dto.SeriesId,
|
||||||
LibraryId = dto.LibraryId,
|
LibraryId = dto.LibraryId,
|
||||||
|
LibraryType = dto.LibraryType,
|
||||||
IsSpecial = dto.IsSpecial,
|
IsSpecial = dto.IsSpecial,
|
||||||
Pages = dto.Pages,
|
Pages = dto.Pages,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
@ -654,4 +655,14 @@ public class LibraryController : BaseApiController
|
||||||
{
|
{
|
||||||
return Ok(await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(libraryId));
|
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()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -246,6 +246,7 @@ public class ReaderController : BaseApiController
|
||||||
SeriesFormat = dto.SeriesFormat,
|
SeriesFormat = dto.SeriesFormat,
|
||||||
SeriesId = dto.SeriesId,
|
SeriesId = dto.SeriesId,
|
||||||
LibraryId = dto.LibraryId,
|
LibraryId = dto.LibraryId,
|
||||||
|
LibraryType = dto.LibraryType,
|
||||||
IsSpecial = dto.IsSpecial,
|
IsSpecial = dto.IsSpecial,
|
||||||
Pages = dto.Pages,
|
Pages = dto.Pages,
|
||||||
SeriesTotalPages = series.Pages,
|
SeriesTotalPages = series.Pages,
|
||||||
|
@ -287,6 +288,7 @@ public class ReaderController : BaseApiController
|
||||||
return Ok(info);
|
return Ok(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns various information about all bookmark files for a Series. Side effect: This will cache the bookmark images for reading.
|
/// Returns various information about all bookmark files for a Series. Side effect: This will cache the bookmark images for reading.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
12
API/DTOs/LibraryTypeDto.cs
Normal file
12
API/DTOs/LibraryTypeDto.cs
Normal 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; }
|
||||||
|
}
|
|
@ -15,4 +15,5 @@ public sealed record BookInfoDto : IChapterInfoDto
|
||||||
public int Pages { get; set; }
|
public int Pages { get; set; }
|
||||||
public bool IsSpecial { get; set; }
|
public bool IsSpecial { get; set; }
|
||||||
public string ChapterTitle { get; set; } = default! ;
|
public string ChapterTitle { get; set; } = default! ;
|
||||||
|
public LibraryType LibraryType { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,5 +14,6 @@ public interface IChapterInfoDto
|
||||||
public int Pages { get; set; }
|
public int Pages { get; set; }
|
||||||
public bool IsSpecial { get; set; }
|
public bool IsSpecial { get; set; }
|
||||||
public string ChapterTitle { get; set; }
|
public string ChapterTitle { get; set; }
|
||||||
|
public LibraryType LibraryType { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,11 +137,11 @@ public class ChapterRepository : IChapterRepository
|
||||||
LibraryId = data.LibraryId,
|
LibraryId = data.LibraryId,
|
||||||
Pages = data.Pages,
|
Pages = data.Pages,
|
||||||
ChapterTitle = data.TitleName,
|
ChapterTitle = data.TitleName,
|
||||||
LibraryType = data.LibraryType
|
LibraryType = data.LibraryType,
|
||||||
})
|
})
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
.SingleOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
return chapterInfo;
|
return chapterInfo;
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,6 +58,7 @@ public interface ILibraryRepository
|
||||||
Task<bool> GetAllowsScrobblingBySeriesId(int seriesId);
|
Task<bool> GetAllowsScrobblingBySeriesId(int seriesId);
|
||||||
|
|
||||||
Task<IDictionary<int, LibraryType>> GetLibraryTypesBySeriesIdsAsync(IList<int> seriesIds);
|
Task<IDictionary<int, LibraryType>> GetLibraryTypesBySeriesIdsAsync(IList<int> seriesIds);
|
||||||
|
Task<IEnumerable<LibraryTypeDto>> GetLibraryTypesAsync(int userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class LibraryRepository : ILibraryRepository
|
public class LibraryRepository : ILibraryRepository
|
||||||
|
@ -369,4 +370,17 @@ public class LibraryRepository : ILibraryRepository
|
||||||
})
|
})
|
||||||
.ToDictionaryAsync(entity => entity.Id, entity => entity.Type);
|
.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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,4 +34,9 @@ public enum LibraryType
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Description("Comic")]
|
[Description("Comic")]
|
||||||
ComicVine = 5,
|
ComicVine = 5,
|
||||||
|
/// <summary>
|
||||||
|
/// Uses Magazine regex and is restricted to PDF and Archive by default
|
||||||
|
/// </summary>
|
||||||
|
[Description("Magazine")]
|
||||||
|
Magazine = 6
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ public class ReadingItemService : IReadingItemService
|
||||||
private readonly ImageParser _imageParser;
|
private readonly ImageParser _imageParser;
|
||||||
private readonly BookParser _bookParser;
|
private readonly BookParser _bookParser;
|
||||||
private readonly PdfParser _pdfParser;
|
private readonly PdfParser _pdfParser;
|
||||||
|
private readonly MagazineParser _magazineParser;
|
||||||
|
|
||||||
public ReadingItemService(IArchiveService archiveService, IBookService bookService, IImageService imageService,
|
public ReadingItemService(IArchiveService archiveService, IBookService bookService, IImageService imageService,
|
||||||
IDirectoryService directoryService, ILogger<ReadingItemService> logger)
|
IDirectoryService directoryService, ILogger<ReadingItemService> logger)
|
||||||
|
@ -42,6 +43,7 @@ public class ReadingItemService : IReadingItemService
|
||||||
_bookParser = new BookParser(directoryService, bookService, _basicParser);
|
_bookParser = new BookParser(directoryService, bookService, _basicParser);
|
||||||
_comicVineParser = new ComicVineParser(directoryService);
|
_comicVineParser = new ComicVineParser(directoryService);
|
||||||
_pdfParser = new PdfParser(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));
|
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))
|
if (_pdfParser.IsApplicable(path, type))
|
||||||
{
|
{
|
||||||
return _pdfParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path));
|
return _pdfParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path));
|
||||||
|
|
|
@ -33,7 +33,6 @@ public interface ISeriesService
|
||||||
Task<bool> UpdateRelatedSeries(UpdateRelatedSeriesDto dto);
|
Task<bool> UpdateRelatedSeries(UpdateRelatedSeriesDto dto);
|
||||||
Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId);
|
Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId);
|
||||||
Task<string> FormatChapterTitle(int userId, ChapterDto chapter, LibraryType libraryType, bool withHash = true);
|
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,
|
Task<string> FormatChapterTitle(int userId, bool isSpecial, LibraryType libraryType, string chapterRange, string? chapterTitle,
|
||||||
bool withHash);
|
bool withHash);
|
||||||
Task<string> FormatChapterName(int userId, LibraryType libraryType, bool withHash = false);
|
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)
|
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)
|
if (isSpecial)
|
||||||
{
|
{
|
||||||
|
@ -643,9 +642,10 @@ public class SeriesService : ISeriesService
|
||||||
var hashSpot = withHash ? "#" : string.Empty;
|
var hashSpot = withHash ? "#" : string.Empty;
|
||||||
var baseChapter = libraryType switch
|
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.LightNovel => await _localizationService.Translate(userId, "book-num", chapterRange),
|
||||||
LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", hashSpot, 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.ComicVine => await _localizationService.Translate(userId, "issue-num", hashSpot, chapterRange),
|
||||||
LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", chapterRange),
|
LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", chapterRange),
|
||||||
LibraryType.Image => 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);
|
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
|
// TODO: Refactor this out and use FormatChapterTitle instead across library
|
||||||
public async Task<string> FormatChapterName(int userId, LibraryType libraryType, bool withHash = false)
|
public async Task<string> FormatChapterName(int userId, LibraryType libraryType, bool withHash = false)
|
||||||
|
|
|
@ -57,7 +57,7 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser
|
||||||
{
|
{
|
||||||
if (!Parser.IsSeriesAndYear(directory)) continue;
|
if (!Parser.IsSeriesAndYear(directory)) continue;
|
||||||
info.Series = directory;
|
info.Series = directory;
|
||||||
info.Volumes = Parser.ParseYear(directory);
|
info.Volumes = Parser.ParseYearFromSeries(directory);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser
|
||||||
if (Parser.IsSeriesAndYear(directoryName))
|
if (Parser.IsSeriesAndYear(directoryName))
|
||||||
{
|
{
|
||||||
info.Series = directoryName;
|
info.Series = directoryName;
|
||||||
info.Volumes = Parser.ParseYear(directoryName);
|
info.Volumes = Parser.ParseYearFromSeries(directoryName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
86
API/Services/Tasks/Scanner/Parser/MagazineParser.cs
Normal file
86
API/Services/Tasks/Scanner/Parser/MagazineParser.cs
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
@ -64,6 +66,8 @@ public static partial class Parser
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const string TagsInBrackets = $@"\[(?!\s){BalancedBracket}(?<!\s)\]";
|
private const string TagsInBrackets = $@"\[(?!\s){BalancedBracket}(?<!\s)\]";
|
||||||
|
|
||||||
|
[GeneratedRegex(@"^\d+$")]
|
||||||
|
private static partial Regex IsNumberRegex();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Matches against font-family css syntax. Does not match if url import has data: starting, as that is binary data
|
/// 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
|
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)
|
public static MangaFormat ParseFormat(string filePath)
|
||||||
|
@ -739,6 +806,20 @@ public static partial class Parser
|
||||||
return string.Empty;
|
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)
|
public static string ParseMangaVolume(string filename)
|
||||||
{
|
{
|
||||||
foreach (var regex in MangaVolumeRegex)
|
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)
|
private static string FormatValue(string value, bool hasPart)
|
||||||
{
|
{
|
||||||
if (!value.Contains('-'))
|
if (!value.Contains('-'))
|
||||||
|
@ -1159,13 +1371,21 @@ public static partial class Parser
|
||||||
return !string.IsNullOrEmpty(name) && SeriesAndYearRegex.IsMatch(name);
|
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;
|
if (string.IsNullOrEmpty(name)) return string.Empty;
|
||||||
var match = SeriesAndYearRegex.Match(name);
|
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)
|
public static string? RemoveExtensionIfSupported(string? filename)
|
||||||
|
|
|
@ -6,10 +6,11 @@ export enum LibraryType {
|
||||||
Book = 2,
|
Book = 2,
|
||||||
Images = 3,
|
Images = 3,
|
||||||
LightNovel = 4,
|
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 {
|
export interface Library {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
|
@ -26,6 +26,8 @@ export class LibraryTypePipe implements PipeTransform {
|
||||||
return this.translocoService.translate('library-type-pipe.manga');
|
return this.translocoService.translate('library-type-pipe.manga');
|
||||||
case LibraryType.LightNovel:
|
case LibraryType.LightNovel:
|
||||||
return this.translocoService.translate('library-type-pipe.lightNovel');
|
return this.translocoService.translate('library-type-pipe.lightNovel');
|
||||||
|
case LibraryType.Magazine:
|
||||||
|
return this.translocoService.translate('library-type-pipe.magazine');
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
|
@ -136,6 +136,20 @@ export class LibraryService {
|
||||||
return this.httpClient.post(this.baseUrl + 'library/update', model);
|
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) {
|
getLibraryType(libraryId: number) {
|
||||||
if (this.libraryTypes != undefined && this.libraryTypes.hasOwnProperty(libraryId)) {
|
if (this.libraryTypes != undefined && this.libraryTypes.hasOwnProperty(libraryId)) {
|
||||||
return of(this.libraryTypes[libraryId]);
|
return of(this.libraryTypes[libraryId]);
|
||||||
|
|
|
@ -647,12 +647,19 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
|
|
||||||
this.bookService.getBookInfo(this.chapterId).subscribe(info => {
|
this.bookService.getBookInfo(this.chapterId).subscribe(info => {
|
||||||
if (this.readingListMode && info.seriesFormat !== MangaFormat.EPUB) {
|
|
||||||
// Redirect to the manga reader.
|
this.libraryService.getLibraryType(this.libraryId).pipe(take(1)).subscribe(type => {
|
||||||
const params = this.readerService.getQueryParamsObject(this.incognitoMode, this.readingListMode, this.readingListId);
|
this.libraryType = type;
|
||||||
this.router.navigate(this.readerService.getNavigationArray(info.libraryId, info.seriesId, this.chapterId, info.seriesFormat), {queryParams: params});
|
this.cdRef.markForCheck();
|
||||||
return;
|
|
||||||
}
|
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.bookTitle = info.bookTitle;
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
|
@ -672,10 +679,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
this.continuousChaptersStack.push(this.chapterId);
|
this.continuousChaptersStack.push(this.chapterId);
|
||||||
|
|
||||||
this.libraryService.getLibraryType(this.libraryId).pipe(take(1)).subscribe(type => {
|
|
||||||
this.libraryType = type;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.updateImageSizes();
|
this.updateImageSizes();
|
||||||
|
|
||||||
if (this.pageNum >= this.maxPages) {
|
if (this.pageNum >= this.maxPages) {
|
||||||
|
|
|
@ -282,7 +282,8 @@ export class ReadingListDetailComponent implements OnInit {
|
||||||
readChapter(item: ReadingListItem) {
|
readChapter(item: ReadingListItem) {
|
||||||
if (!this.readingList) return;
|
if (!this.readingList) return;
|
||||||
const params = this.readerService.getQueryParamsObject(false, true, this.readingList.id);
|
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) {
|
async handleReadingListActionCallback(action: ActionItem<ReadingList>, readingList: ReadingList) {
|
||||||
|
@ -366,7 +367,8 @@ export class ReadingListDetailComponent implements OnInit {
|
||||||
if (!this.readingList) return;
|
if (!this.readingList) return;
|
||||||
const firstItem = this.items[0];
|
const firstItem = this.items[0];
|
||||||
this.router.navigate(
|
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}});
|
{queryParams: {readingListId: this.readingList.id, incognitoMode: incognitoMode}});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -383,7 +385,8 @@ export class ReadingListDetailComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.router.navigate(
|
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}});
|
{queryParams: {readingListId: this.readingList.id, incognitoMode: incognitoMode}});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { HttpParams } from '@angular/common/http';
|
import {HttpParams} from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import { Chapter } from 'src/app/_models/chapter';
|
import {Chapter} from 'src/app/_models/chapter';
|
||||||
import { LibraryType } from 'src/app/_models/library/library';
|
import {LibraryType} from 'src/app/_models/library/library';
|
||||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
import {MangaFormat} from 'src/app/_models/manga-format';
|
||||||
import { PaginatedResult } from 'src/app/_models/pagination';
|
import {PaginatedResult} from 'src/app/_models/pagination';
|
||||||
import { Series } from 'src/app/_models/series';
|
import {Series} from 'src/app/_models/series';
|
||||||
import { Volume } from 'src/app/_models/volume';
|
import {Volume} from 'src/app/_models/volume';
|
||||||
import {translate, TranslocoService} from "@jsverse/transloco";
|
import {translate} from "@jsverse/transloco";
|
||||||
import {debounceTime, ReplaySubject, shareReplay} from "rxjs";
|
import {debounceTime, ReplaySubject, shareReplay} from "rxjs";
|
||||||
|
|
||||||
export enum KEY_CODES {
|
export enum KEY_CODES {
|
||||||
|
@ -73,6 +73,7 @@ export class UtilityService {
|
||||||
return translate('common.book-num' + extra) + (includeSpace ? ' ' : '');
|
return translate('common.book-num' + extra) + (includeSpace ? ' ' : '');
|
||||||
case LibraryType.Comic:
|
case LibraryType.Comic:
|
||||||
case LibraryType.ComicVine:
|
case LibraryType.ComicVine:
|
||||||
|
case LibraryType.Magazine:
|
||||||
if (includeHash) {
|
if (includeHash) {
|
||||||
return translate('common.issue-hash-num');
|
return translate('common.issue-hash-num');
|
||||||
}
|
}
|
||||||
|
|
|
@ -212,6 +212,8 @@ export class SideNavComponent implements OnInit {
|
||||||
return 'fa-book-open';
|
return 'fa-book-open';
|
||||||
case LibraryType.Images:
|
case LibraryType.Images:
|
||||||
return 'fa-images';
|
return 'fa-images';
|
||||||
|
case LibraryType.Magazine:
|
||||||
|
return 'fa-book-open'; // TODO: Find an icon for this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -214,6 +214,12 @@ export class LibrarySettingsModalComponent implements OnInit {
|
||||||
this.libraryForm.get(FileTypeGroup.Pdf + '')?.setValue(false);
|
this.libraryForm.get(FileTypeGroup.Pdf + '')?.setValue(false);
|
||||||
this.libraryForm.get(FileTypeGroup.Epub + '')?.setValue(false);
|
this.libraryForm.get(FileTypeGroup.Epub + '')?.setValue(false);
|
||||||
break;
|
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);
|
this.libraryForm.get('allowScrobbling')?.setValue(this.IsKavitaPlusEligible);
|
||||||
|
|
|
@ -583,7 +583,8 @@
|
||||||
"manga": "Manga",
|
"manga": "Manga",
|
||||||
"comicVine": "Comic",
|
"comicVine": "Comic",
|
||||||
"image": "Image",
|
"image": "Image",
|
||||||
"lightNovel": "Light Novel"
|
"lightNovel": "Light Novel",
|
||||||
|
"magazine": "Magazine"
|
||||||
},
|
},
|
||||||
|
|
||||||
"age-rating-pipe": {
|
"age-rating-pipe": {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue