Implemented a Generic Library that uses Regex from the admin to parse the files. Still a bit of a WIP on how it's actually going to work or if it will be usable.

This commit is contained in:
Joseph Milazzo 2024-03-15 12:26:19 -05:00
parent 9d31262448
commit 6f4162d793
22 changed files with 224 additions and 60 deletions

View file

@ -154,5 +154,25 @@ public class VolumeListExtensionsTests
}
/// <summary>
/// Single volume (comicvine type style) with negative or non-numerical
/// </summary>
public void GetCoverImage_LooseChapters_WithSub1_InOneVolume()
{
var volumes = new List<Volume>()
{
new VolumeBuilder("2")
.WithChapter(new ChapterBuilder("-1").WithCoverImage("Chapter -1").Build())
.WithChapter(new ChapterBuilder("1").WithCoverImage("Chapter 1").Build())
.Build(),
};
// Not testable due to the code not actually doing the heavy lifting
// var actual = volumes.GetCoverImage(MangaFormat.Archive);
// Assert.NotNull(actual);
// Assert.Equal("Chapter 1", actual.CoverImage);
}
#endregion
}

View file

@ -52,12 +52,12 @@ internal class MockReadingItemServiceForCacheService : IReadingItemService
throw new System.NotImplementedException();
}
public ParserInfo Parse(string path, string rootPath, string libraryRoot, LibraryType type)
public ParserInfo Parse(string path, string rootPath, string libraryRoot, Library library)
{
throw new System.NotImplementedException();
}
public ParserInfo ParseFile(string path, string rootPath, string libraryRoot, LibraryType type)
public ParserInfo ParseFile(string path, string rootPath, string libraryRoot, Library library)
{
throw new System.NotImplementedException();
}

View file

@ -54,14 +54,14 @@ internal class MockReadingItemService : IReadingItemService
throw new NotImplementedException();
}
public ParserInfo Parse(string path, string rootPath, string libraryRoot, LibraryType type)
public ParserInfo Parse(string path, string rootPath, string libraryRoot, Library library)
{
return _defaultParser.Parse(path, rootPath, libraryRoot, type);
return _defaultParser.Parse(path, rootPath, libraryRoot, library.Type);
}
public ParserInfo ParseFile(string path, string rootPath, string libraryRoot, LibraryType type)
public ParserInfo ParseFile(string path, string rootPath, string libraryRoot, Library library)
{
return _defaultParser.Parse(path, rootPath, libraryRoot, type);
return _defaultParser.Parse(path, rootPath, libraryRoot, library.Type);
}
}

View file

@ -34,5 +34,10 @@ public enum LibraryType
/// </summary>
[Description("Comic (ComicVine)")]
ComicVine = 5,
/// <summary>
/// This library requires custom regex from admin
/// </summary>
[Description("Generic")]
Generic = 6,
}

View file

@ -43,6 +43,6 @@ public static class ChapterListExtensions
/// <returns></returns>
public static int MinimumReleaseYear(this IList<Chapter> chapters)
{
return chapters.Select(v => v.ReleaseDate.Year).Where(y => NumberHelper.IsValidYear(y)).DefaultIfEmpty().Min();
return chapters.Select(v => v.ReleaseDate.Year).Where(NumberHelper.IsValidYear).DefaultIfEmpty().Min();
}
}

View file

@ -107,10 +107,16 @@ public class MetadataService : IMetadataService
_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, volume.CoverImage),
null, volume.Created, forceUpdate)) return Task.FromResult(false);
// For cover selection, chapters need to try for issue 1 first, then fallback to first sort order
volume.Chapters ??= new List<Chapter>();
var firstChapter = volume.Chapters.MinBy(x => x.MinNumber, ChapterSortComparerDefaultFirst.Default);
var firstChapter = volume.Chapters.FirstOrDefault(x => x.MinNumber.Is(1f));
if (firstChapter == null)
{
firstChapter = volume.Chapters.MinBy(x => x.SortOrder, ChapterSortComparerDefaultFirst.Default);
if (firstChapter == null) return Task.FromResult(false);
}
volume.CoverImage = firstChapter.CoverImage;
_updateEvents.Add(MessageFactory.CoverUpdateEvent(volume.Id, MessageFactoryEntityTypes.Volume));

View file

@ -1,5 +1,6 @@
using System;
using API.Data.Metadata;
using API.Entities;
using API.Entities.Enums;
using API.Services.Tasks.Scanner.Parser;
using Microsoft.Extensions.Logging;
@ -13,7 +14,7 @@ public interface IReadingItemService
int GetNumberOfPages(string filePath, MangaFormat format);
string GetCoverImage(string filePath, string fileName, MangaFormat format, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default);
void Extract(string fileFilePath, string targetDirectory, MangaFormat format, int imageCount = 1);
ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, LibraryType type);
ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, Library library);
}
public class ReadingItemService : IReadingItemService
@ -28,6 +29,7 @@ public class ReadingItemService : IReadingItemService
private readonly ImageParser _imageParser;
private readonly BookParser _bookParser;
private readonly PdfParser _pdfParser;
private readonly GenericLibraryParser _genericLibraryParser;
public ReadingItemService(IArchiveService archiveService, IBookService bookService, IImageService imageService,
IDirectoryService directoryService, ILogger<ReadingItemService> logger)
@ -43,6 +45,7 @@ public class ReadingItemService : IReadingItemService
_bookParser = new BookParser(directoryService, bookService, _basicParser);
_pdfParser = new PdfParser(directoryService);
_basicParser = new BasicParser(directoryService, _imageParser);
_genericLibraryParser = new GenericLibraryParser(directoryService);
}
/// <summary>
@ -71,9 +74,9 @@ public class ReadingItemService : IReadingItemService
/// <param name="path">Path of a file</param>
/// <param name="rootPath"></param>
/// <param name="type">Library type to determine parsing to perform</param>
public ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, LibraryType type)
public ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, Library library)
{
var info = Parse(path, rootPath, libraryRoot, type);
var info = Parse(path, rootPath, libraryRoot, library);
if (info == null)
{
_logger.LogError("Unable to parse any meaningful information out of file {FilePath}", path);
@ -166,27 +169,31 @@ public class ReadingItemService : IReadingItemService
/// <param name="rootPath"></param>
/// <param name="type"></param>
/// <returns></returns>
private ParserInfo? Parse(string path, string rootPath, string libraryRoot, LibraryType type)
private ParserInfo? Parse(string path, string rootPath, string libraryRoot, Library library)
{
if (_comicVineParser.IsApplicable(path, type))
if (_comicVineParser.IsApplicable(path, library.Type))
{
return _comicVineParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path));
return _comicVineParser.Parse(path, rootPath, libraryRoot, library.Type, GetComicInfo(path));
}
if (_imageParser.IsApplicable(path, type))
if (_imageParser.IsApplicable(path, library.Type))
{
return _imageParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path));
return _imageParser.Parse(path, rootPath, libraryRoot, library.Type, GetComicInfo(path));
}
if (_bookParser.IsApplicable(path, type))
if (_bookParser.IsApplicable(path, library.Type))
{
return _bookParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path));
return _bookParser.Parse(path, rootPath, libraryRoot, library.Type, GetComicInfo(path));
}
if (_pdfParser.IsApplicable(path, type))
if (_genericLibraryParser.IsApplicable(path, library.Type))
{
return _pdfParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path));
return _genericLibraryParser.Parse(path, rootPath, libraryRoot, library.Type, GetComicInfo(path));
}
if (_basicParser.IsApplicable(path, type))
if (_pdfParser.IsApplicable(path, library.Type))
{
return _basicParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path));
return _pdfParser.Parse(path, rootPath, libraryRoot, library.Type, GetComicInfo(path));
}
if (_basicParser.IsApplicable(path, library.Type))
{
return _basicParser.Parse(path, rootPath, libraryRoot, library.Type, GetComicInfo(path));
}
return null;

View file

@ -411,7 +411,7 @@ public class ParseScannedFiles
// Multiple Series can exist within a folder. We should instead put these infos on the result and perform merging above
IList<ParserInfo> infos = files
.Select(file => _readingItemService.ParseFile(file, folder, libraryRoot, library.Type))
.Select(file => _readingItemService.ParseFile(file, folder, libraryRoot, library))
.Where(info => info != null)
.ToList()!;

View file

@ -1,4 +1,5 @@
using System.IO;
using System.Collections.Generic;
using System.IO;
using API.Data.Metadata;
using API.Entities.Enums;
@ -11,7 +12,8 @@ namespace API.Services.Tasks.Scanner.Parser;
/// </summary>
public class BasicParser(IDirectoryService directoryService, IDefaultParser imageParser) : DefaultParser(directoryService)
{
public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo? comicInfo = null)
public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type,
ComicInfo? comicInfo = null, IEnumerable<string>? extraRegex = null)
{
var fileName = directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath);
// TODO: Potential Bug: This will return null, but on Image libraries, if all images, we would want to include this.

View file

@ -1,11 +1,13 @@
using API.Data.Metadata;
using System.Collections.Generic;
using API.Data.Metadata;
using API.Entities.Enums;
namespace API.Services.Tasks.Scanner.Parser;
public class BookParser(IDirectoryService directoryService, IBookService bookService, IDefaultParser basicParser) : DefaultParser(directoryService)
{
public override ParserInfo Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo comicInfo = null)
public override ParserInfo Parse(string filePath, string rootPath, string libraryRoot, LibraryType type,
ComicInfo? comicInfo = null, IEnumerable<string>? extraRegex = null)
{
var info = bookService.ParseInfo(filePath);
if (info == null) return null;

View file

@ -1,4 +1,5 @@
using System.IO;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using API.Data.Metadata;
using API.Entities.Enums;
@ -15,11 +16,8 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser
/// <summary>
/// This Parser generates Series name to be defined as Series + first Issue Volume, so "Batman (2020)".
/// </summary>
/// <param name="filePath"></param>
/// <param name="rootPath"></param>
/// <param name="type"></param>
/// <returns></returns>
public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo? comicInfo = null)
public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type,
ComicInfo? comicInfo = null, IEnumerable<string>? extraRegex = null)
{
if (type != LibraryType.ComicVine) return null;

View file

@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using API.Data.Metadata;
@ -8,7 +9,7 @@ namespace API.Services.Tasks.Scanner.Parser;
public interface IDefaultParser
{
ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo? comicInfo = null);
ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo? comicInfo = null, IEnumerable<string>? extraRegex = null);
void ParseFromFallbackFolders(string filePath, string rootPath, LibraryType type, ref ParserInfo ret);
bool IsApplicable(string filePath, LibraryType type);
}
@ -26,8 +27,11 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau
/// <param name="filePath"></param>
/// <param name="rootPath">Root folder</param>
/// <param name="type">Allows different Regex to be used for parsing.</param>
/// <param name="comicInfo">ComicInfo if present (for epub it si always present)</param>
/// <param name="extraRegex">The regex for the Generic Parser</param>
/// <returns><see cref="ParserInfo"/> or null if Series was empty</returns>
public abstract ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo? comicInfo = null);
public abstract ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type,
ComicInfo? comicInfo = null, IEnumerable<string>? extraRegex = null);
/// <summary>
/// Fills out <see cref="ParserInfo"/> by trying to parse volume, chapters, and series from folders

View file

@ -0,0 +1,90 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using API.Data.Metadata;
using API.Entities.Enums;
namespace API.Services.Tasks.Scanner.Parser;
#nullable enable
/// <summary>
/// Uses an at-runtime array of Regex to parse out information
/// </summary>
/// <param name="directoryService"></param>
public class GenericLibraryParser(IDirectoryService directoryService) : DefaultParser(directoryService)
{
public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type,
ComicInfo? comicInfo = null, IEnumerable<string>? extraRegex = null)
{
if (extraRegex == null) return null;
// The idea is this is passed in as a default param. Only Generic will use it
var fileName = directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath);
var info = new ParserInfo()
{
Filename = Path.GetFileName(filePath),
Format = Parser.ParseFormat(filePath),
Title = Parser.RemoveExtensionIfSupported(fileName)!,
FullFilePath = filePath,
Series = string.Empty,
ComicInfo = comicInfo,
Chapters = Parser.ParseComicChapter(fileName),
Volumes = Parser.ParseComicVolume(fileName)
};
foreach (var regex in extraRegex)
{
var matches = new Regex(regex, RegexOptions.IgnoreCase).Matches(fileName);
foreach (var group in matches.Select(match => match.Groups))
{
foreach (var matchKey in group.Keys)
{
var matchValue = group[matchKey].Value;
switch (matchKey)
{
case "Series":
info.Series = SetIfNotDefault(matchValue, info.Series);
break;
case "Chapter":
info.Chapters = SetIfNotDefault(matchValue, info.Chapters);
break;
}
}
}
}
// Process the final info here: (cleaning values, setting internal encoding overrides)
if (info.IsSpecial)
{
info.Volumes = Parser.SpecialVolume;
}
if (string.IsNullOrEmpty(info.Chapters))
{
info.Chapters = Parser.DefaultChapter;
}
if (!info.IsSpecial && string.IsNullOrEmpty(info.Volumes))
{
info.Chapters = Parser.LooseLeafVolume;
}
return string.IsNullOrEmpty(info.Series) ? null : info;
}
private static string SetIfNotDefault(string value, string originalValue)
{
if (string.IsNullOrEmpty(value)) return originalValue;
if (string.IsNullOrEmpty(originalValue)) return value;
return originalValue;
}
public override bool IsApplicable(string filePath, LibraryType type)
{
return type == LibraryType.Generic;
}
}

View file

@ -1,4 +1,5 @@
using System.IO;
using System.Collections.Generic;
using System.IO;
using API.Data.Metadata;
using API.Entities.Enums;
@ -7,7 +8,8 @@ namespace API.Services.Tasks.Scanner.Parser;
public class ImageParser(IDirectoryService directoryService) : DefaultParser(directoryService)
{
public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo? comicInfo = null)
public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type,
ComicInfo? comicInfo = null, IEnumerable<string>? extraRegex = null)
{
if (type != LibraryType.Image || !Parser.IsImage(filePath)) return null;

View file

@ -1,4 +1,5 @@
using System.IO;
using System.Collections.Generic;
using System.IO;
using API.Data.Metadata;
using API.Entities.Enums;
@ -6,7 +7,8 @@ namespace API.Services.Tasks.Scanner.Parser;
public class PdfParser(IDirectoryService directoryService) : DefaultParser(directoryService)
{
public override ParserInfo Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo comicInfo = null)
public override ParserInfo Parse(string filePath, string rootPath, string libraryRoot, LibraryType type,
ComicInfo? comicInfo = null, IEnumerable<string>? extraRegex = null)
{
var fileName = directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath);
var ret = new ParserInfo

View file

@ -6,7 +6,8 @@ export enum LibraryType {
Book = 2,
Images = 3,
LightNovel = 4,
ComicVine = 5
ComicVine = 5,
Generic = 6
}
export interface Library {

View file

@ -24,6 +24,10 @@ export class LibraryTypePipe implements PipeTransform {
return this.translocoService.translate('library-type-pipe.image');
case LibraryType.Manga:
return this.translocoService.translate('library-type-pipe.manga');
case LibraryType.LightNovel:
return this.translocoService.translate('library-type-pipe.lightNovel');
case LibraryType.Generic:
return this.translocoService.translate('library-type-pipe.generic');
default:
return '';
}

View file

@ -65,6 +65,7 @@ export class UtilityService {
switch(libraryType) {
case LibraryType.Book:
case LibraryType.LightNovel:
case LibraryType.Generic:
return this.translocoService.translate('common.book-num') + (includeSpace ? ' ' : '');
case LibraryType.Comic:
case LibraryType.ComicVine:

View file

@ -185,6 +185,7 @@ export class SideNavComponent implements OnInit {
getLibraryTypeIcon(format: LibraryType) {
switch (format) {
case LibraryType.Book:
case LibraryType.Generic:
case LibraryType.LightNovel:
return 'fa-book';
case LibraryType.Comic:

View file

@ -196,6 +196,12 @@ export class LibrarySettingsModalComponent implements OnInit {
this.libraryForm.get(FileTypeGroup.Pdf + '')?.setValue(false);
this.libraryForm.get(FileTypeGroup.Epub + '')?.setValue(false);
break;
case LibraryType.Generic:
this.libraryForm.get(FileTypeGroup.Archive + '')?.setValue(false);
this.libraryForm.get(FileTypeGroup.Images + '')?.setValue(false);
this.libraryForm.get(FileTypeGroup.Pdf + '')?.setValue(true);
this.libraryForm.get(FileTypeGroup.Epub + '')?.setValue(false);
break;
}
}),
takeUntilDestroyed(this.destroyRef)

View file

@ -502,7 +502,9 @@
"comic": "Comic",
"manga": "Manga",
"comicVine": "ComicVine",
"image": "Image"
"image": "Image",
"lightNovel": "Light Novel",
"generic": "Generic"
},
"age-rating-pipe": {

View file

@ -7,7 +7,7 @@
"name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
},
"version": "0.7.14.9"
"version": "0.7.14.10"
},
"servers": [
{
@ -2924,7 +2924,8 @@
2,
3,
4,
5
5,
6
],
"type": "integer",
"format": "int32"
@ -2938,7 +2939,8 @@
2,
3,
4,
5
5,
6
],
"type": "integer",
"format": "int32"
@ -2952,7 +2954,8 @@
2,
3,
4,
5
5,
6
],
"type": "integer",
"format": "int32"
@ -3640,7 +3643,8 @@
2,
3,
4,
5
5,
6
],
"type": "integer",
"format": "int32"
@ -13549,7 +13553,8 @@
2,
3,
4,
5
5,
6
],
"type": "integer",
"format": "int32"
@ -14174,7 +14179,8 @@
2,
3,
4,
5
5,
6
],
"type": "integer",
"description": "Library type",
@ -15965,7 +15971,8 @@
2,
3,
4,
5
5,
6
],
"type": "integer",
"format": "int32"
@ -16080,7 +16087,8 @@
2,
3,
4,
5
5,
6
],
"type": "integer",
"format": "int32"
@ -17082,7 +17090,8 @@
2,
3,
4,
5
5,
6
],
"type": "integer",
"format": "int32"
@ -17136,7 +17145,8 @@
2,
3,
4,
5
5,
6
],
"type": "integer",
"format": "int32"
@ -19528,7 +19538,8 @@
2,
3,
4,
5
5,
6
],
"type": "integer",
"format": "int32"