Compare commits

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

16 commits

Author SHA1 Message Date
Joseph Milazzo
fad70432fb Some testing on Generic library 2024-04-07 14:40:44 -05:00
Joseph Milazzo
5423526484 Merged develop in 2024-04-07 14:13:06 -05:00
Joseph Milazzo
6f4162d793 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. 2024-03-15 12:26:19 -05:00
majora2007
9d31262448 Bump versions by dotnet-bump-version. 2024-03-14 21:04:56 +00:00
Joe Milazzo
353d44a882
ComicVine Finish Line (#2779) 2024-03-14 14:03:53 -07:00
majora2007
4ccac5f479 Bump versions by dotnet-bump-version. 2024-03-12 21:11:26 +00:00
Joe Milazzo
08cc7c7cbd
Scanner Tech Debt (#2777) 2024-03-12 14:10:43 -07:00
majora2007
f5a31b9a02 Bump versions by dotnet-bump-version. 2024-03-10 13:41:47 +00:00
Joe Milazzo
3e813534f9
Comic Rework Bugfixes Round 1 (#2774) 2024-03-10 06:41:01 -07:00
majora2007
d29dd59964 Bump versions by dotnet-bump-version. 2024-03-09 17:37:18 +00:00
Joe Milazzo
fc21073898
Comic Rework (Part 1) (#2772) 2024-03-09 09:36:36 -08:00
majora2007
58c77b32b1 Bump versions by dotnet-bump-version. 2024-03-07 14:14:25 +00:00
Joe Milazzo
fc87dba0a7
Foundational Rework (Round 2) (#2767) 2024-03-07 06:13:36 -08:00
majora2007
304fd8bc79 Bump versions by dotnet-bump-version. 2024-02-26 20:59:03 +00:00
Joe Milazzo
4fa21fe1ca
Foundational Rework (#2745) 2024-02-26 12:56:39 -08:00
majora2007
42cd6e9b3a Bump versions by dotnet-bump-version. 2024-02-26 12:41:19 +00:00
22 changed files with 207 additions and 43 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

@ -193,6 +193,7 @@ public class CollectionTagRepository : ICollectionTagRepository
.Where(t => t.Id == tag.Id)
.SelectMany(uc => uc.Items.Select(s => s.Metadata))
.Select(sm => sm.AgeRating)
.DefaultIfEmpty()
.MaxAsync();
tag.AgeRating = maxAgeRating;
await _context.SaveChangesAsync();

View file

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

View file

@ -40,8 +40,11 @@ public class Library : IEntityDate
/// </summary>
/// <remarks>Scrobbling requires a valid LicenseKey</remarks>
public bool AllowScrobbling { get; set; } = true;
/// <summary>
/// Extra Regex that can be used for parsing
/// </summary>
/// <remarks>This is only used for Generic Library</remarks>
//public string? ExtraParsingRegex { get; set; }

View file

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using API.Entities;
using API.Entities.Enums;
using API.Services.Tasks.Scanner.Parser;

View file

@ -28,6 +28,7 @@ public class ReadingItemService : IReadingItemService
private readonly ImageParser _imageParser;
private readonly BookParser _bookParser;
private readonly PdfParser _pdfParser;
private readonly GenericLibraryParser _genericParser;
public ReadingItemService(IArchiveService archiveService, IBookService bookService, IImageService imageService,
IDirectoryService directoryService, ILogger<ReadingItemService> logger)
@ -43,6 +44,7 @@ public class ReadingItemService : IReadingItemService
_bookParser = new BookParser(directoryService, bookService, _basicParser);
_comicVineParser = new ComicVineParser(directoryService);
_pdfParser = new PdfParser(directoryService);
_genericParser = new GenericLibraryParser(directoryService);
}
@ -177,6 +179,10 @@ public class ReadingItemService : IReadingItemService
/// <returns></returns>
private ParserInfo? Parse(string path, string rootPath, string libraryRoot, LibraryType type)
{
if (_genericParser.IsApplicable(path, type))
{
return _genericParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path));
}
if (_comicVineParser.IsApplicable(path, type))
{
return _comicVineParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path));

View file

@ -416,15 +416,12 @@ public class ParseScannedFiles
var folder = result.Folder;
var libraryRoot = result.LibraryRoot;
// When processing files for a folder and we do enter, we need to parse the information and combine parser infos
// NOTE: We might want to move the merge step later in the process, like return and combine.
_logger.LogDebug("[ScannerService] Found {Count} files for {Folder}", files.Count, folder);
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
MessageFactory.FileScanProgressEvent($"{files.Count} files in {folder}", library.Name, ProgressEventType.Updated));
if (files.Count == 0)
{
_logger.LogInformation("[ScannerService] {Folder} is empty, no longer in this location, or has no file types that match Library File Types", folder);
result.ParserInfos = ArraySegment<ParserInfo>.Empty;
_logger.LogInformation("[ScannerService] {Folder} is empty or is no longer in this location", folder);
return;
}

View file

@ -1,4 +1,5 @@
using System.IO;
using System.Collections.Generic;
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, BasicParser basicParser) : DefaultParser(directoryService)
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,12 @@ 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="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,99 @@
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;
// It can be very difficult for the user to supply all the regex needed to properly parse, we might need to let them override (but not sure how this will work)
extraRegex = new List<string>()
{
@"(?<Series>.*)(\b|_)v(?<Volume>\d+-?\d+)( |_)"
};
// 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, Parser.MatchOptions, Parser.RegexTimeout).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;
case "Volume":
info.Volumes = SetIfNotDefault(matchValue, info.Volumes);
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 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

@ -36,7 +36,7 @@ public static class Parser
public const string SupportedExtensions =
ArchiveFileExtensions + "|" + ImageFileExtensions + "|" + BookFileExtensions;
private const RegexOptions MatchOptions =
public const RegexOptions MatchOptions =
RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant;
private static readonly ImmutableArray<string> FormatTagSpecialKeywords = ImmutableArray.Create(
@ -1149,7 +1149,7 @@ public static class Parser
public static string? ExtractFilename(string fileUrl)
{
var matches = Parser.CssImageUrlRegex.Matches(fileUrl);
var matches = CssImageUrlRegex.Matches(fileUrl);
foreach (Match match in matches)
{
if (!match.Success) continue;

View file

@ -1,4 +1,5 @@
using System.IO;
using System.Collections.Generic;
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

@ -1,12 +1,13 @@
import {FileTypeGroup} from "./file-type-group.enum";
export enum LibraryType {
Manga = 0,
Comic = 1,
Book = 2,
Images = 3,
LightNovel = 4,
ComicVine = 5
Manga = 0,
Comic = 1,
Book = 2,
Images = 3,
LightNovel = 4,
ComicVine = 5,
Generic = 6
}
export interface Library {

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.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

@ -200,6 +200,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;
}
this.libraryForm.get('allowScrobbling')?.setValue(this.IsKavitaPlusEligible);

View file

@ -518,7 +518,8 @@
"manga": "Manga",
"comicVine": "ComicVine",
"image": "Image",
"lightNovel": "Light Novel"
"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.12"
"version": "0.7.14.13"
},
"servers": [
{
@ -3138,7 +3138,8 @@
2,
3,
4,
5
5,
6
],
"type": "integer",
"format": "int32"
@ -3152,7 +3153,8 @@
2,
3,
4,
5
5,
6
],
"type": "integer",
"format": "int32"
@ -3166,7 +3168,8 @@
2,
3,
4,
5
5,
6
],
"type": "integer",
"format": "int32"
@ -3854,7 +3857,8 @@
2,
3,
4,
5
5,
6
],
"type": "integer",
"format": "int32"
@ -14157,7 +14161,8 @@
2,
3,
4,
5
5,
6
],
"type": "integer",
"format": "int32"
@ -14782,7 +14787,8 @@
2,
3,
4,
5
5,
6
],
"type": "integer",
"description": "Library type",
@ -16616,7 +16622,8 @@
2,
3,
4,
5
5,
6
],
"type": "integer",
"format": "int32"
@ -16651,6 +16658,7 @@
},
"created": {
"type": "string",
"description": "Extra Regex that can be used for parsing",
"format": "date-time"
},
"lastModified": {
@ -16731,7 +16739,8 @@
2,
3,
4,
5
5,
6
],
"type": "integer",
"format": "int32"
@ -17802,7 +17811,8 @@
2,
3,
4,
5
5,
6
],
"type": "integer",
"format": "int32"
@ -17856,7 +17866,8 @@
2,
3,
4,
5
5,
6
],
"type": "integer",
"format": "int32"
@ -20248,7 +20259,8 @@
2,
3,
4,
5
5,
6
],
"type": "integer",
"format": "int32"